/*-------------------------------------------------------------------------
 *
 * partprune.c
 *		支持在查询规划和执行期间进行分区修剪
 *
 * 本模块使用表的分区描述符、查询子句和运行时参数中包含的信息
 * 实现分区修剪。
 *
 * 在规划期间，可以与表的分区键匹配的子句将被转换为一组“修剪步骤”，
 * 然后执行这些步骤以识别满足步骤中约束条件的分区集合（作为
 * RelOptInfo->part_rels 数组中的索引）。不在该集合中的分区
 * 被称为已经修剪。
 *
 * 基本修剪步骤可能涉及在执行期间才知道值的表达式，如参数，
 * 在这种情况下，修剪不能完全在规划期间进行。在这种情况下，这种步骤
 * 将与计划一起包含，以便它们可以被执行者用于进一步修剪。
 *
 * 有两种类型的修剪步骤。“基本”修剪步骤表示在分区键列上的测试，
 * 通常是与表达式进行比较。“组合”修剪步骤表示布尔连接器（AND/OR），
 * 并使用适当的组合方法组合一些前置步骤的输出。
 *
 * 有关步骤生成的更多详细信息，请参见 gen_partprune_steps_internal()。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *		  src/backend/partitioning/partprune.c
 *
 *-------------------------------------------------------------------------
*/
#include "postgres.h"

#include "access/hash.h"
#include "access/nbtree.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "parser/parsetree.h"
#include "partitioning/partbounds.h"
#include "partitioning/partprune.h"
#include "rewrite/rewriteManip.h"
#include "utils/array.h"
#include "utils/lsyscache.h"


/*
 * 关于与分区键匹配的子句的信息。
 */
typedef struct PartClauseInfo
{
	int			keyno;			/* 分区键编号（0 到 partnatts - 1） */
	Oid			opno;			/* 用于将 partkey 与 expr 进行比较的操作符 */
	bool		op_is_ne;		/* 子句的原始操作符是 <> 吗？ */
	Expr	   *expr;			/* partition key 所比较的 expr */
	Oid			cmpfn;			/* 将 'expr' 与分区键进行比较的函数的 Oid */
	int			op_strategy;	/* 确定操作符的 btree 策略 */
} PartClauseInfo;

/*
 * PartClauseMatchStatus
 *		描述 match_clause_to_partition_key() 的结果
 */
typedef enum PartClauseMatchStatus
{
	PARTCLAUSE_NOMATCH,
	PARTCLAUSE_MATCH_CLAUSE,
	PARTCLAUSE_MATCH_NULLNESS,
	PARTCLAUSE_MATCH_STEPS,
	PARTCLAUSE_MATCH_CONTRADICT,
	PARTCLAUSE_UNSUPPORTED
} PartClauseMatchStatus;

/*
 * PartClauseTarget
 *		标识我们可以用于生成修剪步骤的 qual 子句
 */
typedef enum PartClauseTarget
{
	PARTTARGET_PLANNER,			/* 希望在规划期间进行修剪 */
	PARTTARGET_INITIAL,			/* 希望在执行器启动时进行修剪 */
	PARTTARGET_EXEC				/* 希望在每个计划节点扫描期间进行修剪 */
} PartClauseTarget;

/*
 * GeneratePruningStepsContext
 *		有关为给定子句集生成“修剪步骤”的当前状态的信息
 *
 * gen_partprune_steps() 初始化并返回此结构的实例。
 *
 * 注意，如果我们发现任何具有这些属性的潜在有用的修剪子句，
 * 無論我們是否實際上在步驟列表中使用了該子句，has_mutable_op、has_mutable_arg 和 has_exec_param 都會被設置。
 * 该定义允许我们在某些情况下跳过 PARTTARGET_EXEC 传递。
 */
typedef struct GeneratePruningStepsContext
{
	/* gen_partprune_steps 的输入参数的副本： */
	RelOptInfo *rel;			/* 分区关系 */
	PartClauseTarget target;	/* 我们正在为其生成步骤的用例 */
	/* 结果数据： */
	List	   *steps;			/* PartitionPruneSteps 的列表 */
	bool		has_mutable_op; /* 子句包含任何稳定的操作符 */
	bool		has_mutable_arg;	/* 子句包含任何可变比较值，*而不是* exec 参数 */
	bool		has_exec_param; /* 子句包含任何 PARAM_EXEC 参数 */
	bool		contradictory;	/* 子句被证明是自相矛盾的 */
	/* 工作状态： */
	int			next_step_id;
} GeneratePruningStepsContext;

/* 执行一个 PartitionPruneStep 的结果 */
typedef struct PruneStepResult
{
	/*
	 * 通过修剪步骤选择的分区对应的边界的偏移量
	 * （在表的 boundinfo 中）。
	 */
	Bitmapset  *bound_offsets;

	bool		scan_default;	/* 扫描默认分区吗？ */
	bool		scan_null;		/* 扫描 NULL 值的分区吗？ */
} PruneStepResult;


static List *fc_add_part_relids(List *fc_allpartrelids, Bitmapset *fc_partrelids);
static List *fc_make_partitionedrel_pruneinfo(PlannerInfo *fc_root,
										   RelOptInfo *fc_parentrel,
										   List *fc_prunequal,
										   Bitmapset *fc_partrelids,
										   int *fc_relid_subplan_map,
										   Bitmapset **fc_matchedsubplans);
static void fc_gen_partprune_steps(RelOptInfo *fc_rel, List *fc_clauses,
								PartClauseTarget fc_target,
								GeneratePruningStepsContext *fc_context);
static List *fc_gen_partprune_steps_internal(GeneratePruningStepsContext *fc_context,
										  List *fc_clauses);
static PartitionPruneStep *fc_gen_prune_step_op(GeneratePruningStepsContext *fc_context,
											 StrategyNumber fc_opstrategy, bool fc_op_is_ne,
											 List *fc_exprs, List *fc_cmpfns, Bitmapset *fc_nullkeys);
static PartitionPruneStep *fc_gen_prune_step_combine(GeneratePruningStepsContext *fc_context,
												  List *fc_source_stepids,
												  PartitionPruneCombineOp fc_combineOp);
static List *fc_gen_prune_steps_from_opexps(GeneratePruningStepsContext *fc_context,
										 List **fc_keyclauses, Bitmapset *fc_nullkeys);
static PartClauseMatchStatus fc_match_clause_to_partition_key(GeneratePruningStepsContext *fc_context,
														   Expr *fc_clause, Expr *fc_partkey, int fc_partkeyidx,
														   bool *fc_clause_is_not_null,
														   PartClauseInfo **fc_pc, List **fc_clause_steps);
static List *fc_get_steps_using_prefix(GeneratePruningStepsContext *fc_context,
									StrategyNumber fc_step_opstrategy,
									bool fc_step_op_is_ne,
									Expr *fc_step_lastexpr,
									Oid fc_step_lastcmpfn,
									Bitmapset *fc_step_nullkeys,
									List *fc_prefix);
static List *fc_get_steps_using_prefix_recurse(GeneratePruningStepsContext *fc_context,
											StrategyNumber fc_step_opstrategy,
											bool fc_step_op_is_ne,
											Expr *fc_step_lastexpr,
											Oid fc_step_lastcmpfn,
											Bitmapset *fc_step_nullkeys,
											List *fc_prefix,
											ListCell *fc_start,
											List *fc_step_exprs,
											List *fc_step_cmpfns);
static PruneStepResult *fc_get_matching_hash_bounds(PartitionPruneContext *fc_context,
												 StrategyNumber fc_opstrategy, Datum *fc_values, int fc_nvalues,
												 FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys);
static PruneStepResult *fc_get_matching_list_bounds(PartitionPruneContext *fc_context,
												 StrategyNumber fc_opstrategy, Datum fc_value, int fc_nvalues,
												 FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys);
static PruneStepResult *fc_get_matching_range_bounds(PartitionPruneContext *fc_context,
												  StrategyNumber fc_opstrategy, Datum *fc_values, int fc_nvalues,
												  FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys);
static Bitmapset *fc_pull_exec_paramids(Expr *fc_expr);
static bool fc_pull_exec_paramids_walker(Node *fc_node, Bitmapset **fc_context);
static Bitmapset *fc_get_partkey_exec_paramids(List *fc_steps);
static PruneStepResult *fc_perform_pruning_base_step(PartitionPruneContext *fc_context,
												  PartitionPruneStepOp *fc_opstep);
static PruneStepResult *fc_perform_pruning_combine_step(PartitionPruneContext *fc_context,
													 PartitionPruneStepCombine *fc_cstep,
													 PruneStepResult **fc_step_results);
static PartClauseMatchStatus fc_match_boolean_partition_clause(Oid fc_partopfamily,
															Expr *fc_clause,
															Expr *fc_partkey,
															Expr **fc_outconst,
															bool *fc_noteq);
static void fc_partkey_datum_from_expr(PartitionPruneContext *fc_context,
									Expr *fc_expr, int fc_stateidx,
									Datum *fc_value, bool *fc_isnull);


/*
 * make_partition_pruneinfo
 *		构建 PartitionPruneInfo，可在执行器中使用，以允许进行
 *		额外的分区修剪。 当分区修剪毫无用处时返回 NULL。
 *
 * 'parentrel' 是 appendrel 的 RelOptInfo，'subpaths' 是其子关系的扫描路径列表。
 * 'prunequal' 是潜在修剪限定条件的列表（即适用于 appendrel 的限制子句）。
 */
PartitionPruneInfo *
make_partition_pruneinfo(PlannerInfo *fc_root, RelOptInfo *fc_parentrel,
						 List *fc_subpaths,
						 List *fc_prunequal)
{
	PartitionPruneInfo *fc_pruneinfo;
	Bitmapset  *fc_allmatchedsubplans = NULL;
	List	   *fc_allpartrelids;
	List	   *fc_prunerelinfos;
	int		   *fc_relid_subplan_map;
	ListCell   *fc_lc;
	int			fc_i;

	/*
	 * 扫描子路径以查看哪些是分区子关系的扫描，并识别其父分区关系。 
	 * （注意：我们必须将父分区关系限制为parentrel或parentrel的子关系，
	 * 否则我们无法将prunequal翻译为匹配。）
	 *
	 * 还构造一个临时数组，用于将分区子关系的relid映射到该分区的扫描计划中'subpaths'的索引。
	 * （使用"subplan"而不是"subpath"有点不准确，但我们将其保留。） 
	 * 为了方便，我们在这里使用1-based索引，这样零可以表示未填充的数组条目。
	 */
	fc_allpartrelids = NIL;
	fc_relid_subplan_map = palloc0(sizeof(int) * fc_root->simple_rel_array_size);

	fc_i = 1;
	foreach(fc_lc, fc_subpaths)
	{
		Path	   *fc_path = (Path *) lfirst(fc_lc);
		RelOptInfo *fc_pathrel = fc_path->parent;

		/* 我们这里不考虑分区连接 */
		if (fc_pathrel->reloptkind == RELOPT_OTHER_MEMBER_REL)
		{
			RelOptInfo *fc_prel = fc_pathrel;
			Bitmapset  *fc_partrelids = NULL;

			/*
			 * 遍历到pathrel的最上层分区父级， 
			 * 在此过程中收集父relids；但如果达到parentrel就停止。 
			 * （通常，pathrel的最上层分区父级要么是parentrel，
			 * 要么是parentrel的UNION ALL appendrel子级。 
			 * 但在处理多层分区树的分区连接时，
			 * 我们可以看到一个附加路径，其parentrel是一个中间分区表。)
			 */
			do
			{
				AppendRelInfo *fc_appinfo;

				Assert(fc_prel->relid < fc_root->simple_rel_array_size);
				fc_appinfo = fc_root->append_rel_array[fc_prel->relid];
				fc_prel = find_base_rel(fc_root, fc_appinfo->parent_relid);
				if (!IS_PARTITIONED_REL(fc_prel))
					break;		/* 到达一个非分区父级 */
				/* 将此级别视为一个有趣的父级 */
				fc_partrelids = bms_add_member(fc_partrelids, fc_prel->relid);
				if (fc_prel == fc_parentrel)
					break;		/* 不要遍历超过parentrel */
			} while (fc_prel->reloptkind == RELOPT_OTHER_MEMBER_REL);

			if (fc_partrelids)
			{
				/*
				 * 找到了一些相关的父分区，这些父分区可能与我们已经找到的分区树重叠或不重叠。 
				 * 将新信息添加到allpartrelids列表中。
				 */
				fc_allpartrelids = fc_add_part_relids(fc_allpartrelids, fc_partrelids);
				/* 还要在relid_subplan_map[]中记录子计划 */
				/* 请不要重复 */
				Assert(fc_relid_subplan_map[fc_pathrel->relid] == 0);
				fc_relid_subplan_map[fc_pathrel->relid] = fc_i;
			}
		}
		fc_i++;
	}

	/*
	 * 现在我们为每个最上层分区关系构建一个PartitionedRelPruneInfo 
	 * （略去那些事实证明没有有用修剪条件的）。 
	 */
	fc_prunerelinfos = NIL;
	foreach(fc_lc, fc_allpartrelids)
	{
		Bitmapset  *fc_partrelids = (Bitmapset *) lfirst(fc_lc);
		List	   *fc_pinfolist;
		Bitmapset  *fc_matchedsubplans = NULL;

		fc_pinfolist = fc_make_partitionedrel_pruneinfo(fc_root, fc_parentrel,
												  fc_prunequal,
												  fc_partrelids,
												  fc_relid_subplan_map,
												  &fc_matchedsubplans);

		/* 当修剪可能时，记录匹配的子计划 */
		if (fc_pinfolist != NIL)
		{
			fc_prunerelinfos = lappend(fc_prunerelinfos, fc_pinfolist);
			fc_allmatchedsubplans = bms_join(fc_matchedsubplans,
										  fc_allmatchedsubplans);
		}
	}

	pfree(fc_relid_subplan_map);

	/*
	 * 如果没有任何分区层级具有有用的运行时修剪条件，
	 * 那么我们可以不再考虑运行时修剪。
	 */
	if (fc_prunerelinfos == NIL)
		return NULL;

	/* 否则构建结果数据结构 */
	fc_pruneinfo = makeNode(PartitionPruneInfo);
	fc_pruneinfo->prune_infos = fc_prunerelinfos;

	/*
	 * 一些子计划可能不属于任何已识别的分区关系。 
	 * 这可能发生在包含非分区表的UNION ALL查询中， 
	 * 或者当某些层级不是运行时可修剪的。 
	 * 构建一个包含所有此类子计划索引的位图集，
	 * 以便执行器可以识别哪些子计划永远不应被修剪。
	 */
	if (bms_num_members(fc_allmatchedsubplans) < list_length(fc_subpaths))
	{
		Bitmapset  *fc_other_subplans;

		/* 创建allmatchedsubplans的补集 */
		fc_other_subplans = bms_add_range(NULL, 0, list_length(fc_subpaths) - 1);
		fc_other_subplans = bms_del_members(fc_other_subplans, fc_allmatchedsubplans);

		fc_pruneinfo->other_subplans = fc_other_subplans;
	}
	else
		fc_pruneinfo->other_subplans = NULL;

	return fc_pruneinfo;
}

/*
 * add_part_relids
 *     向分区关系ID的Bitmap集列表中添加新信息。
 *
 * 在'allpartrelids'中， 每个最上层的父分区关系都有一个Bitmap集。
 * 每个Bitmap集包含最上层父关系的RT索引以及其相关的非叶子子分区。
 * 由于（根据rangetable列表的构造）父分区的RT索引必须低于它们的子分区，
 * 我们可以通过Bitmap集中最低设置的位来区分最上层父分区。
 *
 * 'partrelids'包含一个父分区关系的RT索引，以及可能的一些非叶子子分区，
 * 这些是新识别出来的作为传递给make_partition_pruneinfo()的一些子路径关系的父关系。
 * 这些被添加到'allpartrelids'的适当成员中。
 *
 * 注意，该列表仅包含分区表的RT索引，这些表是出现在make_partition_pruneinfo()
 * 处理中某些扫描级关系的父关系。此外，“最上层”父关系不允许高于与附加路径关联的'parentrel'。
 * 通过这种方式，我们避免在不能为当前问题提供有用修剪信息的分区关系上浪费周期。
 * （'parentrel'可能是子分区表，且扫描级关系可能是子分区表而不是叶子分区。
 * 因此，我们必须参考我们所处理的特定附加路径来构建这个关系集，而不是查看
 * 在RelOptInfos中表示的完整分区结构。）
 */
static List * fc_add_part_relids(List *fc_allpartrelids, Bitmapset *fc_partrelids)
{
	Index		fc_targetpart;
	ListCell   *fc_lc;

	/* 我们可以很容易地通过这种方式获取最低设置的位： */
	fc_targetpart = bms_next_member(fc_partrelids, -1);
	Assert(fc_targetpart > 0);

	/* 寻找匹配的最上层父关系 */
	foreach(fc_lc, fc_allpartrelids)
	{
		Bitmapset  *fc_currpartrelids = (Bitmapset *) lfirst(fc_lc);
		Index		fc_currtarget = bms_next_member(fc_currpartrelids, -1);

		if (fc_targetpart == fc_currtarget)
		{
			/* 找到匹配项，因此将任何新的RT索引添加到此层次结构 */
			fc_currpartrelids = bms_add_members(fc_currpartrelids, fc_partrelids);
			lfirst(fc_lc) = fc_currpartrelids;
			return fc_allpartrelids;
		}
	}
	/* 没有匹配项，因此将新的分区层次结构添加到列表 */
	return lappend(fc_allpartrelids, fc_partrelids);
}

/*
 * make_partitionedrel_pruneinfo
 *     为分区层次结构中的每个有趣的分区关系构建一个PartitionedRelPruneInfos的列表。
 *     这些可以在执行器中用于允许额外的分区修剪发生。
 *
 * parentrel：与考虑的附加路径关联的关系
 * prunequal：表示为parentrel的潜在修剪条件
 * partrelids：标识单个分区层次结构中的相关分区表的RT索引集合
 * relid_subplan_map[]：将子关系的relids映射到子计划索引
 * matchedsubplans：成功时，接收与此分区层次结构匹配的子计划索引集合
 *
 * 如果我们找不到任何有用的运行时修剪步骤，返回NIL。
 * 然而，成功时，在partrelids中识别的每个关系将有一个结果列表中的元素，即使它们中的某些是无用的。
 */
static List * fc_make_partitionedrel_pruneinfo(PlannerInfo *fc_root, RelOptInfo *fc_parentrel,
							  List *fc_prunequal,
							  Bitmapset *fc_partrelids,
							  int *fc_relid_subplan_map,
							  Bitmapset **fc_matchedsubplans)
{
	RelOptInfo *fc_targetpart = NULL;
	List	   *fc_pinfolist = NIL;
	bool		fc_doruntimeprune = false;
	int		   *fc_relid_subpart_map;
	Bitmapset  *fc_subplansfound = NULL;
	ListCell   *fc_lc;
	int			fc_rti;
	int			fc_i;

	/*
	 * 检查每个分区的关系，构建一个临时数组以映射
	 * 从规划器的 relids 到分区关系的索引，并为每个分区关系构建
	 * PartitionedRelPruneInfo。
	 *
	 * 在这个阶段，我们发现是否完全需要运行时修剪；如果不需要，我们可以避免进一步的工作。
	 */
	fc_relid_subpart_map = palloc0(sizeof(int) * fc_root->simple_rel_array_size);

	fc_i = 1;
	fc_rti = -1;
	while ((fc_rti = bms_next_member(fc_partrelids, fc_rti)) > 0)
	{
		RelOptInfo *fc_subpart = find_base_rel(fc_root, fc_rti);
		PartitionedRelPruneInfo *fc_pinfo;
		List	   *fc_partprunequal;
		List	   *fc_initial_pruning_steps;
		List	   *fc_exec_pruning_steps;
		Bitmapset  *fc_execparamids;
		GeneratePruningStepsContext fc_context;

		/*
		 * 填充映射数组。
		 *
		 * relid_subpart_map 将非叶子分区的 relid 映射到返回的 PartitionedRelPruneInfo 列表中该分区信息的索引。
		 * 我们在这里使用基于 1 的索引，因此零可以表示未填充的数组条目。
		 */
		Assert(fc_rti < fc_root->simple_rel_array_size);
		fc_relid_subpart_map[fc_rti] = fc_i++;

		/*
		 * 如果必要，为该分区转换修剪条件。
		 *
		 * 列表中的第一个项目是目标分区关系。
		 */
		if (!fc_targetpart)
		{
			fc_targetpart = fc_subpart;

			/*
			 * prunequal 被呈现给我们作为 'parentrel' 的条件。
			 * 通常这个关系与 targetpart 是相同的，因此我们可以跳过
			 * adjust_appendrel_attrs 步骤。但它可能不是相同的，这样我们就必须进行转换。
			 * 我们在这里更新 prunequal 参数，因为在后续子分区的循环迭代中，
			 * 我们想要从父变量转换为子变量。
			 */
			if (!bms_equal(fc_parentrel->relids, fc_subpart->relids))
			{
				int			fc_nappinfos;
				AppendRelInfo **fc_appinfos = find_appinfos_by_relids(fc_root,
																   fc_subpart->relids,
																   &fc_nappinfos);

				fc_prunequal = (List *) adjust_appendrel_attrs(fc_root, (Node *)
															fc_prunequal,
															fc_nappinfos,
															fc_appinfos);

				pfree(fc_appinfos);
			}

			fc_partprunequal = fc_prunequal;
		}
		else
		{
			/*
			 * 对于子分区表，列的顺序可能与父表不同，
			 * 因此我们必须转换 prunequal 以使其与此关系兼容。
			 */
			fc_partprunequal = (List *)
				adjust_appendrel_attrs_multilevel(fc_root,
												  (Node *) fc_prunequal,
												  fc_subpart->relids,
												  fc_targetpart->relids);
		}

		/*
		 * 将修剪条件转换为修剪步骤。我们可能需要两次这样做，
		 * 一次是为了获得执行器启动修剪步骤，另一次是为了
		 * 执行器逐扫描修剪步骤。第一次通过创建启动
		 * 修剪步骤并检测是否有任何可能有用的条件
		 * 需要逐扫描修剪。
		 */
		fc_gen_partprune_steps(fc_subpart, fc_partprunequal, PARTTARGET_INITIAL,
							&fc_context);

		if (fc_context.contradictory)
		{
			/*
			 * 这不应该发生，因为规划器应该早些时候检测到这一点。
			 * 然而，我们确实在这里使用来自参数化路径的额外条件。
			 * 这些仅将 Params 与分区键进行比较，
			 * 因此这不应该导致发现任何之前未发现的新条件矛盾，因为
			 * 在规划期间 Param 值是未知的。无论如何，我们最好在这里
			 * 做一些合理的事情，所以我们就禁用运行时修剪吧。
			 */
			return NIL;
		}

		/*
		 * 如果在可用的修剪条件中没有可变运算符或表达式，
		 * 那么运行启动修剪是没有意义的，因为计划时修剪应该已经修剪了所有可以修剪的内容。
		 */
		if (fc_context.has_mutable_op || fc_context.has_mutable_arg)
			fc_initial_pruning_steps = fc_context.steps;
		else
			fc_initial_pruning_steps = NIL;

		/*
		 * 如果在潜在可用的修剪条件中没有 exec Params，
		 * 那么甚至不需要考虑逐扫描修剪。
		 */
		if (fc_context.has_exec_param)
		{
			/* ... 好吧，我们最好考虑一下 */
			fc_gen_partprune_steps(fc_subpart, fc_partprunequal, PARTTARGET_EXEC,
								&fc_context);

			if (fc_context.contradictory)
			{
				/* 如上所述，如果发生任何可疑情况，则跳过运行时修剪 */
				return NIL;
			}

			fc_exec_pruning_steps = fc_context.steps;

			/*
			 * 检测哪些 exec Params 实际上被使用了；尽管一些
			 * 在可用条件中，但这并不意味着我们实际使用了它们。
			 * 如果没有，就跳过逐扫描修剪。
			 */
			fc_execparamids = fc_get_partkey_exec_paramids(fc_exec_pruning_steps);

			if (bms_is_empty(fc_execparamids))
				fc_exec_pruning_steps = NIL;
		}
		else
		{
			/* 到处都没有 exec Params，因此忘记扫描时修剪吧 */
			fc_exec_pruning_steps = NIL;
			fc_execparamids = NULL;
		}

		if (fc_initial_pruning_steps || fc_exec_pruning_steps)
			fc_doruntimeprune = true;

		/* 开始构建这个关系的 PartitionedRelPruneInfo */
		fc_pinfo = makeNode(PartitionedRelPruneInfo);
		fc_pinfo->rtindex = fc_rti;
		fc_pinfo->initial_pruning_steps = fc_initial_pruning_steps;
		fc_pinfo->exec_pruning_steps = fc_exec_pruning_steps;
		fc_pinfo->execparamids = fc_execparamids;
		/* 剩余字段将在下一个循环中填充 */

		fc_pinfolist = lappend(fc_pinfolist, fc_pinfo);
	}

	if (!fc_doruntimeprune)
	{
		/* 不需要运行时剪枝。 */
		pfree(fc_relid_subpart_map);
		return NIL;
	}

	/*
	 * 需要运行时剪枝，因此初始化其他信息。
	 * 这包括两个映射 -- 一个用于将叶分区的分区索引转换为
	 * 子计划列表中对应子计划的索引，
	 * 另一个用于将子分区的分区索引转换为其在
	 * PartitionedRelPruneInfo 列表中的 PartitionedRelPruneInfo 的索引。
	 */
	foreach(fc_lc, fc_pinfolist)
	{
		PartitionedRelPruneInfo *fc_pinfo = lfirst(fc_lc);
		RelOptInfo *fc_subpart = find_base_rel(fc_root, fc_pinfo->rtindex);
		Bitmapset  *fc_present_parts;
		int			fc_nparts = fc_subpart->nparts;
		int		   *fc_subplan_map;
		int		   *fc_subpart_map;
		Oid		   *fc_relid_map;

		/*
		 * 为这个分区级别构建子计划和子分区映射。
		 * 这里我们转换为以零为基础的索引，空条目用 -1 表示。
		 * 还构建一个包含所有存在分区的 Bitmapset（即，并未被剪枝的分区）。
		 */
		fc_subplan_map = (int *) palloc(fc_nparts * sizeof(int));
		memset(fc_subplan_map, -1, fc_nparts * sizeof(int));
		fc_subpart_map = (int *) palloc(fc_nparts * sizeof(int));
		memset(fc_subpart_map, -1, fc_nparts * sizeof(int));
		fc_relid_map = (Oid *) palloc0(fc_nparts * sizeof(Oid));
		fc_present_parts = NULL;

		fc_i = -1;
		while ((fc_i = bms_next_member(fc_subpart->live_parts, fc_i)) >= 0)
		{
			RelOptInfo *fc_partrel = fc_subpart->part_rels[fc_i];
			int			fc_subplanidx;
			int			fc_subpartidx;

			Assert(fc_partrel != NULL);

			fc_subplan_map[fc_i] = fc_subplanidx = fc_relid_subplan_map[fc_partrel->relid] - 1;
			fc_subpart_map[fc_i] = fc_subpartidx = fc_relid_subpart_map[fc_partrel->relid] - 1;
			fc_relid_map[fc_i] = planner_rt_fetch(fc_partrel->relid, fc_root)->relid;
			if (fc_subplanidx >= 0)
			{
				fc_present_parts = bms_add_member(fc_present_parts, fc_i);

				/* 记录找到这个子计划 */
				fc_subplansfound = bms_add_member(fc_subplansfound, fc_subplanidx);
			}
			else if (fc_subpartidx >= 0)
				fc_present_parts = bms_add_member(fc_present_parts, fc_i);
		}

		/*
		 * 确保没有为分区表生成多余的 PartitionedRelPruneInfo，
		 * 这些表没有子路径或子 PartitionedRelPruneInfo。
		 */
		Assert(!bms_is_empty(fc_present_parts));

		/* 记录映射和其他信息。 */
		fc_pinfo->present_parts = fc_present_parts;
		fc_pinfo->nparts = fc_nparts;
		fc_pinfo->subplan_map = fc_subplan_map;
		fc_pinfo->subpart_map = fc_subpart_map;
		fc_pinfo->relid_map = fc_relid_map;
	}

	pfree(fc_relid_subpart_map);

	*fc_matchedsubplans = fc_subplansfound;

	return fc_pinfolist;
}

/*
 * gen_partprune_steps
 *	 处理 'clauses'（通常是一个关系的 baserestrictinfo 列表）
 *	 并创建一个“分区剪枝步骤”的列表。
 *
 * 'target' 指示是为计划生成剪枝步骤（仅使用
 * 不可变的子句），还是为执行器启动生成（使用任何允许的
 * 子句，除了包含 PARAM_EXEC 参数的子句），或者为执行器
 * 每次扫描剪枝生成（使用任何允许的子句）。
 *
 * 'context' 是一个输出参数，它接收步骤列表以及
 * 一些辅助标志；请参阅 GeneratePruningStepsContext 类型定义。
 */
static void fc_gen_partprune_steps(RelOptInfo *fc_rel, List *fc_clauses, PartClauseTarget fc_target,
					GeneratePruningStepsContext *fc_context)
{
	/* 初始化所有输出值为零/假/空 */ 
	memset(fc_context, 0, sizeof(GeneratePruningStepsContext));
	fc_context->rel = fc_rel;
	fc_context->target = fc_target;

	/*
	 * 如果这个分区表又是一个分区，并且与其父级共享任何
	 * 分区键，那么它的层级可能允许父级的值范围比它的某些
	 * 分区（特别是默认分区）更窄。这通常没有用，但可以用于
	 * 剪枝默认分区。
	 */
	if (partition_bound_has_default(fc_rel->boundinfo) && fc_rel->partition_qual)
	{
		/* 进行复制以避免修改传入的列表 */
		fc_clauses = list_concat_copy(fc_clauses, fc_rel->partition_qual);
	}

	/* 进入复杂的情况。 */
	(void) fc_gen_partprune_steps_internal(fc_context, fc_clauses);
}

/*
 * prune_append_rel_partitions
 *	 处理关系的 baserestrictinfo，并利用在查询规划期间
 *	 可评估的条件，以确定必须扫描以满足这些条件的最小
 *	 分区集。返回匹配分区的 Bitmapset，其中包含
 *	 关系的 part_rels 数组中的分区索引。
 *
 * 调用者必须确保 'rel' 是一个分区表。
 */
Bitmapset * prune_append_rel_partitions(RelOptInfo *fc_rel)
{
	List	   *fc_clauses = fc_rel->baserestrictinfo;
	List	   *fc_pruning_steps;
	GeneratePruningStepsContext fc_gcontext;
	PartitionPruneContext fc_context;

	Assert(fc_rel->part_scheme != NULL);

	/* 如果没有分区，返回空集 */
	if (fc_rel->nparts == 0)
		return NULL;

	/*
	 * 如果禁用剪枝或没有可以剪枝的子句，返回
	 * 所有分区。
	 */
	if (!enable_partition_pruning || fc_clauses == NIL)
		return bms_add_range(NULL, 0, fc_rel->nparts - 1);

	/*
	 * 处理子句以提取在计划时可用的剪枝步骤。
	 * 如果发现子句是矛盾的，我们可以返回空集。
	 */
	fc_gen_partprune_steps(fc_rel, fc_clauses, PARTTARGET_PLANNER,
						&fc_gcontext);
	if (fc_gcontext.contradictory)
		return NULL;
	fc_pruning_steps = fc_gcontext.steps;

	/* 如果没有可用的结果，返回所有分区 */
	if (fc_pruning_steps == NIL)
		return bms_add_range(NULL, 0, fc_rel->nparts - 1);

	/* 设置 PartitionPruneContext */
	fc_context.strategy = fc_rel->part_scheme->strategy;
	fc_context.partnatts = fc_rel->part_scheme->partnatts;
	fc_context.nparts = fc_rel->nparts;
	fc_context.boundinfo = fc_rel->boundinfo;
	fc_context.partcollation = fc_rel->part_scheme->partcollation;
	fc_context.partsupfunc = fc_rel->part_scheme->partsupfunc;
	fc_context.stepcmpfuncs = (FmgrInfo *) palloc0(sizeof(FmgrInfo) *
												fc_context.partnatts *
												list_length(fc_pruning_steps));
	fc_context.ppccontext = CurrentMemoryContext;

	/* 在从计划程序调用时，这些无效 */
	fc_context.planstate = NULL;
	fc_context.exprcontext = NULL;
	fc_context.exprstates = NULL;

	/* 实际的修剪发生在这里。 */
	return get_matching_partitions(&fc_context, fc_pruning_steps);
}

/*
 * get_matching_partitions
 *		确定在分区修剪后存活的分区
 *
 * 注意：当修剪步骤以其他目标生成时，context->exprcontext 必须有效，且不是 PARTTARGET_PLANNER。
 *
 * 返回一个 Bitmapset，包含 RelOptInfo->part_rels 中存活分区的索引。
 */
Bitmapset * get_matching_partitions(PartitionPruneContext *fc_context, List *fc_pruning_steps)
{
	Bitmapset  *fc_result;
	int			fc_num_steps = list_length(fc_pruning_steps),
				fc_i;
	PruneStepResult **fc_results,
			   *fc_final_result;
	ListCell   *fc_lc;
	bool		fc_scan_default;

	/* 如果没有修剪步骤，则所有分区匹配。 */
	if (fc_num_steps == 0)
	{
		Assert(fc_context->nparts > 0);
		return bms_add_range(NULL, 0, fc_context->nparts - 1);
	}

	/*
	 * 为每个修剪步骤分配空间以存储其结果。每个
	 * 槽将在执行给定的修剪步骤后保存一个 PruneStepResult。
	 * 后来的步骤可以使用一个或多个早期步骤的结果。
	 * 应用所有修剪步骤的结果是最后一个修剪步骤的槽中
	 * 包含的值。
	 */
	fc_results = (PruneStepResult **)
		palloc0(fc_num_steps * sizeof(PruneStepResult *));
	foreach(fc_lc, fc_pruning_steps)
	{
		PartitionPruneStep *fc_step = lfirst(fc_lc);

		switch (nodeTag(fc_step))
		{
			case T_PartitionPruneStepOp:
				fc_results[fc_step->step_id] =
					fc_perform_pruning_base_step(fc_context,
											  (PartitionPruneStepOp *) fc_step);
				break;

			case T_PartitionPruneStepCombine:
				fc_results[fc_step->step_id] =
					fc_perform_pruning_combine_step(fc_context,
												 (PartitionPruneStepCombine *) fc_step,
												 fc_results);
				break;

			default:
				elog(ERROR, "invalid pruning step type: %d",
					 (int) nodeTag(fc_step));
		}
	}

	/*
	 * 在此时，我们知道所有需要在结果中包含的 
	 * 数据的偏移量，包括特殊的接受空值的 
	 * 和默认分区。现在收集实际的分区索引。
	 */
	fc_final_result = fc_results[fc_num_steps - 1];
	Assert(fc_final_result != NULL);
	fc_i = -1;
	fc_result = NULL;
	fc_scan_default = fc_final_result->scan_default;
	while ((fc_i = bms_next_member(fc_final_result->bound_offsets, fc_i)) >= 0)
	{
		int			fc_partindex;

		Assert(fc_i < fc_context->boundinfo->nindexes);
		fc_partindex = fc_context->boundinfo->indexes[fc_i];

		if (fc_partindex < 0)
		{
			/*
			 * 在范围分区的情况下，如果分区索引为 -1，
			 * 则表示该偏移量的界限是一个范围的上界，
			 * 并且该范围不被任何分区覆盖（除了可能的
			 * 默认分区）。在哈希分区中，含义相同，
			 * 表示为对应的余数值未定义任何分区。
			 *
			 * 在任何情况下，该值仍然是查询范围的一部分，
			 * 因此如果存在，则标记以扫描默认分区。
			 */
			fc_scan_default |= partition_bound_has_default(fc_context->boundinfo);
			continue;
		}

		fc_result = bms_add_member(fc_result, fc_partindex);
	}

	/* 如果需要和存在，添加空值和/或默认分区。 */
	if (fc_final_result->scan_null)
	{
		Assert(fc_context->strategy == PARTITION_STRATEGY_LIST);
		Assert(partition_bound_accepts_nulls(fc_context->boundinfo));
		fc_result = bms_add_member(fc_result, fc_context->boundinfo->null_index);
	}
	if (fc_scan_default)
	{
		Assert(fc_context->strategy == PARTITION_STRATEGY_LIST ||
			   fc_context->strategy == PARTITION_STRATEGY_RANGE);
		Assert(partition_bound_has_default(fc_context->boundinfo));
		fc_result = bms_add_member(fc_result, fc_context->boundinfo->default_index);
	}

	return fc_result;
}

/*
 * gen_partprune_steps_internal
 *		处理“clauses”以生成分区修剪步骤的列表。当没有生成步骤时，我们返回 NIL。
 *
 * 这些分区修剪步骤有两种形式：操作步骤和组合步骤。
 *
 * 操作步骤（PartitionPruneStepOp）包含我们确定可以用于分区修剪的子句的详细信息。这些包含与分区键进行比较的表达式的详细信息以及比较函数。
 *
 * 组合步骤（PartitionPruneStepCombine）指示分区修剪代码如何从多个输入操作和其他组合步骤生成一组分区。PARTPRUNE_COMBINE_INTERSECT 类型的组合步骤将合并其输入步骤以生成一个仅包含所有输入操作步骤中存在的分区的结果。PARTPRUNE_COMBINE_UNION 组合步骤将生成一个包含每个输入操作步骤中所有分区的结果。
 *
 * 对于 BoolExpr 子句，每个参数都会递归处理。处理 OR BoolExpr 生成的步骤将使用 PARTPRUNE_COMBINE_UNION 合并。AND BoolExpr 将使用 PARTPRUNE_COMBINE_INTERSECT 合并。
 *
 * 否则，我们收到的子句列表假设是互相 AND 的。我们根据这些子句生成所有可以的修剪步骤，然后在最后，如果我们有超过 1 步，我们将每步与 PARTPRUNE_COMBINE_INTERSECT 组合步骤结合。单个步骤原样返回。
 *
 * 如果我们发现子句互相矛盾，或者与分区约束矛盾，或者是包含 false 的伪常量子句，我们将 context->contradictory 设置为 true 并返回 NIL（即没有修剪步骤）。调用者应该在这种情况下考虑所有分区都已被修剪。
 */
static List * fc_gen_partprune_steps_internal(GeneratePruningStepsContext *fc_context,
							 List *fc_clauses)
{
	PartitionScheme fc_part_scheme = fc_context->rel->part_scheme;
	List	   *fc_keyclauses[PARTITION_MAX_KEYS];
	Bitmapset  *fc_nullkeys = NULL,
			   *fc_notnullkeys = NULL;
	bool		fc_generate_opsteps = false;
	List	   *fc_result = NIL;
	ListCell   *fc_lc;

	/*
	 * 如果这个分区关系有一个默认分区并且本身是一个分区（如 partition_qual 不为 NIL 所示），
	 * 我们首先检查子句是否与分区约束矛盾。如果有矛盾，
	 * 则没有必要生成任何步骤，因为已经证明不需要扫描任何分区。
	 *
	 * 这是最后的手段，仅在无法使用与父级分区约束矛盾的子句生成的步骤进行默认分区的修剪时使用；
	 * 当没有默认分区时，常规修剪（成本较低）就足够了。
	 */
	if (partition_bound_has_default(fc_context->rel->boundinfo) &&
		predicate_refuted_by(fc_context->rel->partition_qual, fc_clauses, false))
	{
		fc_context->contradictory = true;
		return NIL;
	}

	memset(fc_keyclauses, 0, sizeof(fc_keyclauses));
	foreach(fc_lc, fc_clauses)
	{
		Expr	   *fc_clause = (Expr *) lfirst(fc_lc);
		int			fc_i;

		/* 查看 RestrictInfo（如果有的话） */
		if (IsA(fc_clause, RestrictInfo))
			fc_clause = ((RestrictInfo *) fc_clause)->clause;

		/* 常量 false 或 null 是矛盾的 */
		if (IsA(fc_clause, Const) &&
			(((Const *) fc_clause)->constisnull ||
			 !DatumGetBool(((Const *) fc_clause)->constvalue)))
		{
			fc_context->contradictory = true;
			return NIL;
		}

		/* 将 BoolExpr 暂时搁置。 */
		if (IsA(fc_clause, BoolExpr))
		{
			/*
			 * 为参数生成步骤。
			 *
			 * 虽然为参数本身生成的步骤将在递归期间添加到 context->steps 中并将独立评估，
			 * 但收集它们的步骤 ID 以存储在我们将要创建的组合步骤中。
			 */
			if (is_orclause(fc_clause))
			{
				List	   *fc_arg_stepids = NIL;
				bool		fc_all_args_contradictory = true;
				ListCell   *fc_lc1;

				/*
				 * 我们可以与递归调用共享外部上下文区域，但矛盾最好还不存在。
				 */
				Assert(!fc_context->contradictory);

				/*
				 * 获取每个参数的修剪步骤。如果我们对所有参数都得到矛盾，这意味着整个OR表达式为假。
				 */
				foreach(fc_lc1, ((BoolExpr *) fc_clause)->args)
				{
					Expr	   *fc_arg = lfirst(fc_lc1);
					bool		fc_arg_contradictory;
					List	   *fc_argsteps;

					fc_argsteps = fc_gen_partprune_steps_internal(fc_context,
															list_make1(fc_arg));
					fc_arg_contradictory = fc_context->contradictory;
					/* 在我们完成之前保持context->contradictory清晰 */
					fc_context->contradictory = false;

					if (fc_arg_contradictory)
					{
						/* 只需忽略自我矛盾的参数。 */
						continue;
					}
					else
						fc_all_args_contradictory = false;

					if (fc_argsteps != NIL)
					{
						/*
						 * gen_partprune_steps_internal()在生成多个步骤时始终添加一个合并步骤，因此
						 * 在这里我们可以只关注列表中的最后一个。如果它仅生成一个，那么列表中的最后一个仍然是我们想要的。
						 */
						PartitionPruneStep *fc_last = llast(fc_argsteps);

						fc_arg_stepids = lappend_int(fc_arg_stepids, fc_last->step_id);
					}
					else
					{
						PartitionPruneStep *fc_orstep;

						/*
						 * 参数不包含与此分区键匹配的子句。我们无法使用这样的参数进行修剪。
						 * 为了向修剪代码指示这一点，我们必须构造一个虚拟的PartitionPruneStepCombine，其
						 * source_stepids设置为一个空列表。
						 */
						fc_orstep = fc_gen_prune_step_combine(fc_context, NIL,
														PARTPRUNE_COMBINE_UNION);
						fc_arg_stepids = lappend_int(fc_arg_stepids, fc_orstep->step_id);
					}
				}

				/* 如果所有OR分支都是矛盾的，我们可以停止 */
				if (fc_all_args_contradictory)
				{
					fc_context->contradictory = true;
					return NIL;
				}

				if (fc_arg_stepids != NIL)
				{
					PartitionPruneStep *fc_step;

					fc_step = fc_gen_prune_step_combine(fc_context, fc_arg_stepids,
												  PARTPRUNE_COMBINE_UNION);
					fc_result = lappend(fc_result, fc_step);
				}
				continue;
			}
			else if (is_andclause(fc_clause))
			{
				List	   *fc_args = ((BoolExpr *) fc_clause)->args;
				List	   *fc_argsteps;

				/*
				 * 参数本身可能包含任意类型的子句，因此只需递归，然后使用合并步骤组合组件分区集。
				 */
				fc_argsteps = fc_gen_partprune_steps_internal(fc_context, fc_args);

				/* 如果任何AND分支是矛盾的，我们可以立即停止 */
				if (fc_context->contradictory)
					return NIL;

				/*
				 * gen_partprune_steps_internal()在生成多个步骤时始终添加一个合并步骤，因此在这里我们可以只关注列表中的最后一个。如果它刚刚
				 * 生成一个，那么列表中的最后一个仍然是我们想要的。
				 */
				if (fc_argsteps != NIL)
					fc_result = lappend(fc_result, llast(fc_argsteps));

				continue;
			}

			/*
			 * 针对NOT子句的继续处理，如果它是布尔子句，将在match_clause_to_partition_key()中处理。我们
			 * 当前不对更复杂的NOT子句执行任何修剪。
			 */
		}

		/*
		 * 查看我们是否可以将此子句与任何分区键匹配。
		 */
		for (fc_i = 0; fc_i < fc_part_scheme->partnatts; fc_i++)
		{
			Expr	   *fc_partkey = linitial(fc_context->rel->partexprs[fc_i]);
			bool		fc_clause_is_not_null = false;
			PartClauseInfo *fc_pc = NULL;
			List	   *fc_clause_steps = NIL;

			switch (fc_match_clause_to_partition_key(fc_context,
												  fc_clause, fc_partkey, fc_i,
												  &fc_clause_is_not_null,
												  &fc_pc, &fc_clause_steps))
			{
				case PARTCLAUSE_MATCH_CLAUSE:
					Assert(fc_pc != NULL);

					/*
					 * 由于我们只允许严格运算符，检查任何矛盾的IS NULL。
					 */
					if (bms_is_member(fc_i, fc_nullkeys))
					{
						fc_context->contradictory = true;
						return NIL;
					}
					fc_generate_opsteps = true;
					fc_keyclauses[fc_i] = lappend(fc_keyclauses[fc_i], fc_pc);
					break;

				case PARTCLAUSE_MATCH_NULLNESS:
					if (!fc_clause_is_not_null)
					{
						/*
						 * 检查冲突的IS NOT NULL以及矛盾的严格子句
						 */
						if (bms_is_member(fc_i, fc_notnullkeys) ||
							fc_keyclauses[fc_i] != NIL)
						{
							fc_context->contradictory = true;
							return NIL;
						}
						fc_nullkeys = bms_add_member(fc_nullkeys, fc_i);
					}
					else
					{
						/* 检查冲突的IS NULL */
						if (bms_is_member(fc_i, fc_nullkeys))
						{
							fc_context->contradictory = true;
							return NIL;
						}
						fc_notnullkeys = bms_add_member(fc_notnullkeys, fc_i);
					}
					break;

				case PARTCLAUSE_MATCH_STEPS:
					Assert(fc_clause_steps != NIL);
					fc_result = list_concat(fc_result, fc_clause_steps);
					break;

				case PARTCLAUSE_MATCH_CONTRADICT:
					/* 如果发现矛盾，我们就没有更多的事情可做了。 */
					fc_context->contradictory = true;
					return NIL;

				case PARTCLAUSE_NOMATCH:

					/*
					 * 子句没有匹配这个键，但它可能匹配下一个。
					 */
					continue;

				case PARTCLAUSE_UNSUPPORTED:
					/* 此子句不能用于修剪。 */
					break;
			}

			/* 完成；去检查下一个子句。 */
			break;
		}
	}

	/*-----------
	 * 现在生成一些（更多的）剪枝步骤。我们有三种策略：
	 *
	 * 1) 根据 IS NULL 子句生成剪枝步骤：
	 *   a) 对于列表分区，null 分区键只能在指定的接受 null 的分区中找到，
	 *      所以如果有包含分区键的 IS NULL 子句，我们应该生成一个剪枝
	 *      步骤，去掉除了那个分区之外的所有分区。我们可以
	 *      忽略可能找到的任何 OpExpr。
	 *   b) 对于范围分区，只有默认分区可以包含 NULL 值，因此相同的理由适用。
	 *   c) 对于哈希分区，只有在我们对所有键都有 IS NULL 子句时，才应用此策略。
	 *      下面的策略 2 将处理一些键有 OpExpr 而其他键有 IS NULL 子句的情况。
	 *
	 * 2) 如果没有，根据我们拥有的 OpExpr 生成步骤（如果有的话）。
	 *
	 * 3) 如果这也不行，如果我们对所有分区键都有 IS NOT NULL 子句，
	 *    我们可能能够生成步骤来修剪仅接受 null 的分区（如果存在的话）。
	 */
	if (!bms_is_empty(fc_nullkeys) &&
		(fc_part_scheme->strategy == PARTITION_STRATEGY_LIST ||
		 fc_part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
		 (fc_part_scheme->strategy == PARTITION_STRATEGY_HASH &&
		  bms_num_members(fc_nullkeys) == fc_part_scheme->partnatts)))
	{
		PartitionPruneStep *fc_step;

		/* 策略 1 */
		fc_step = fc_gen_prune_step_op(fc_context, InvalidStrategy,
								 false, NIL, NIL, fc_nullkeys);
		fc_result = lappend(fc_result, fc_step);
	}
	else if (fc_generate_opsteps)
	{
		List	   *fc_opsteps;

		/* 策略 2 */
		fc_opsteps = fc_gen_prune_steps_from_opexps(fc_context, fc_keyclauses, fc_nullkeys);
		fc_result = list_concat(fc_result, fc_opsteps);
	}
	else if (bms_num_members(fc_notnullkeys) == fc_part_scheme->partnatts)
	{
		PartitionPruneStep *fc_step;

		/* 策略 3 */
		fc_step = fc_gen_prune_step_op(fc_context, InvalidStrategy,
								 false, NIL, NIL, NULL);
		fc_result = lappend(fc_result, fc_step);
	}

	/*
	 * 最后，如果有多个步骤，因为 'clauses' 是相互 AND 的，
	 * 添加一个 INTERSECT 步骤来组合由它们产生的分区集，
	 * 并将其附加到结果列表。
	 */
	if (list_length(fc_result) > 1)
	{
		List	   *fc_step_ids = NIL;
		PartitionPruneStep *fc_final;

		foreach(fc_lc, fc_result)
		{
			PartitionPruneStep *fc_step = lfirst(fc_lc);

			fc_step_ids = lappend_int(fc_step_ids, fc_step->step_id);
		}

		fc_final = fc_gen_prune_step_combine(fc_context, fc_step_ids,
									   PARTPRUNE_COMBINE_INTERSECT);
		fc_result = lappend(fc_result, fc_final);
	}

	return fc_result;
}

/*
 * gen_prune_step_op
 *		为特定操作生成一个剪枝步骤
 *
 * 该步骤被分配一个唯一的步骤标识符并添加到上下文的 'steps'
 * 列表中。
 */
static PartitionPruneStep *
fc_gen_prune_step_op(GeneratePruningStepsContext *fc_context,
				  StrategyNumber fc_opstrategy, bool fc_op_is_ne,
				  List *fc_exprs, List *fc_cmpfns,
				  Bitmapset *fc_nullkeys)
{
	PartitionPruneStepOp *fc_opstep = makeNode(PartitionPruneStepOp);

	fc_opstep->step.step_id = fc_context->next_step_id++;

	/*
	 * 对于包含 <> 运算符的子句，将 opstrategy 设置为
	 * InvalidStrategy，以通知 get_matching_list_bounds 做正确的
	 * 事情。
	 */
	fc_opstep->opstrategy = fc_op_is_ne ? InvalidStrategy : fc_opstrategy;
	Assert(list_length(fc_exprs) == list_length(fc_cmpfns));
	fc_opstep->exprs = fc_exprs;
	fc_opstep->cmpfns = fc_cmpfns;
	fc_opstep->nullkeys = fc_nullkeys;

	fc_context->steps = lappend(fc_context->steps, fc_opstep);

	return (PartitionPruneStep *) fc_opstep;
}

/*
 * gen_prune_step_combine
 *		为多个其他步骤的组合生成一个剪枝步骤
 *
 * 该步骤被分配一个唯一的步骤标识符并添加到上下文的
 * 'steps' 列表中。
 */
static PartitionPruneStep *
fc_gen_prune_step_combine(GeneratePruningStepsContext *fc_context,
					   List *fc_source_stepids,
					   PartitionPruneCombineOp fc_combineOp)
{
	PartitionPruneStepCombine *fc_cstep = makeNode(PartitionPruneStepCombine);

	fc_cstep->step.step_id = fc_context->next_step_id++;
	fc_cstep->combineOp = fc_combineOp;
	fc_cstep->source_stepids = fc_source_stepids;

	fc_context->steps = lappend(fc_context->steps, fc_cstep);

	return (PartitionPruneStep *) fc_cstep;
}

/*
 * gen_prune_steps_from_opexps
 *		生成并返回一个基于 OpExpr 和 BooleanTest 子句的
 *		PartitionPruneStepOp 列表，这些子句已匹配到
 *		分区键上。
 *
 * 'keyclauses' 是一个指向 List 的指针数组，按分区键的
 * 索引索引。数组中的每个 List 元素可以包含匹配
 * 对应分区键列的子句。没有任何匹配子句的分区键列将会有一个空 List。
 *
 * 一些分区策略允许当我们仅对分区键列的前缀有子句时仍然进行剪枝，
 * 例如 RANGE 分区。其他策略，例如 HASH 分区，则需要
 * 所有分区键列都有子句。
 *
 * 当我们在这里返回多个剪枝步骤时，由调用方决定添加相关的“组合”
 * 步骤以组合返回的步骤。这里不执行此操作，因为调用方可能希望在
 * 合并所有步骤之前包含其他剪枝步骤。
 */
static List * fc_gen_prune_steps_from_opexps(GeneratePruningStepsContext *fc_context,
							List **fc_keyclauses, Bitmapset *fc_nullkeys)
{
	PartitionScheme fc_part_scheme = fc_context->rel->part_scheme;
	List	   *fc_opsteps = NIL;
	List	   *fc_btree_clauses[BTMaxStrategyNumber + 1],
			   *fc_hash_clauses[HTMaxStrategyNumber + 1];
	int			fc_i;
	ListCell   *fc_lc;

	memset(fc_btree_clauses, 0, sizeof(fc_btree_clauses));
	memset(fc_hash_clauses, 0, sizeof(fc_hash_clauses));
	for (fc_i = 0; fc_i < fc_part_scheme->partnatts; fc_i++)
	{
		List	   *fc_clauselist = fc_keyclauses[fc_i];
		bool		fc_consider_next_key = true;

		/*  
		 * 对于范围分区，如果当前键没有子句，  
		 * 那么我们也不能考虑任何后续键，因此可以停止。  
		 */  
		if (fc_part_scheme->strategy == PARTITION_STRATEGY_RANGE &&
			fc_clauselist == NIL)
			break;

		/*  
		 * 对于哈希分区，如果某列没有必要的  
		 * 等式子句，则应该有一个 IS NULL 子句，否则  
		 * 无法进行剪枝。  
		 */  
		if (fc_part_scheme->strategy == PARTITION_STRATEGY_HASH &&
			fc_clauselist == NIL && !bms_is_member(fc_i, fc_nullkeys))
			return NIL;

		foreach(fc_lc, fc_clauselist)
		{
			PartClauseInfo *fc_pc = (PartClauseInfo *) lfirst(fc_lc);
			Oid			fc_lefttype,
						fc_righttype;

			/* 查找操作符的 btree/hash 策略编号。 */  
			if (fc_pc->op_strategy == InvalidStrategy)
				get_op_opfamily_properties(fc_pc->opno,
										   fc_part_scheme->partopfamily[fc_i],
										   false,
										   &fc_pc->op_strategy,
										   &fc_lefttype,
										   &fc_righttype);

			switch (fc_part_scheme->strategy)
			{
				case PARTITION_STRATEGY_LIST:
				case PARTITION_STRATEGY_RANGE:
					fc_btree_clauses[fc_pc->op_strategy] =
						lappend(fc_btree_clauses[fc_pc->op_strategy], fc_pc);

					/*  
					 * 如果当前键的子句包含非包含性  
					 * 操作符，我们不能考虑后续分区键。  
					 */  
					if (fc_pc->op_strategy == BTLessStrategyNumber ||
						fc_pc->op_strategy == BTGreaterStrategyNumber)
						fc_consider_next_key = false;
					break;

				case PARTITION_STRATEGY_HASH:
					if (fc_pc->op_strategy != HTEqualStrategyNumber)
						elog(ERROR, "invalid clause for hash partitioning");
					fc_hash_clauses[fc_pc->op_strategy] =
						lappend(fc_hash_clauses[fc_pc->op_strategy], fc_pc);
					break;

				default:
					elog(ERROR, "invalid partition strategy: %c",
						 fc_part_scheme->strategy);
					break;
			}
		}

		/*  
		 * 如果我们已经决定后续分区键的子句  
		 * 对剪枝没有用，不要再搜索了。  
		 */  
		if (!fc_consider_next_key)
			break;
	}

	/*  
	 * 现在，我们已经根据它们的操作符策略  
	 * 将子句进行了划分。检查每个策略  
	 * 是否可以通过收集一系列表达式生成剪枝步骤，这些表达式的值将构成一个向量  
	 * 可用于作为分区边界搜索函数的查找键。  
	 */  
	switch (fc_part_scheme->strategy)
	{
		case PARTITION_STRATEGY_LIST:
		case PARTITION_STRATEGY_RANGE:
			{
				List	   *fc_eq_clauses = fc_btree_clauses[BTEqualStrategyNumber];
				List	   *fc_le_clauses = fc_btree_clauses[BTLessEqualStrategyNumber];
				List	   *fc_ge_clauses = fc_btree_clauses[BTGreaterEqualStrategyNumber];
				int			fc_strat;

				/*  
				 * 对于考虑的每个策略下的子句，  
				 * 我们从早期键的子句中收集操作符策略为包含性的表达式，  
				 * 并放入一个叫做 'prefix' 的列表中。通过将子句自己的表达式追加到  
				 * 'prefix'，我们将使用所生成的  
				 * 向量生成一个步骤，并将当前策略分配给它。实际上，  
				 * 'prefix' 可能包含同一键的多个子句，  
				 * 在这种情况下，我们必须为不同键的不同表达式组合生成步骤，  
				 * get_steps_using_prefix 为我们处理这些组合。  
				 */  
				for (fc_strat = 1; fc_strat <= BTMaxStrategyNumber; fc_strat++)
				{
					foreach(fc_lc, fc_btree_clauses[fc_strat])
					{
						PartClauseInfo *fc_pc = lfirst(fc_lc);
						ListCell   *fc_eq_start;
						ListCell   *fc_le_start;
						ListCell   *fc_ge_start;
						ListCell   *fc_lc1;
						List	   *fc_prefix = NIL;
						List	   *fc_pc_steps;
						bool		fc_prefix_valid = true;
						bool		fc_pk_has_clauses;
						int			fc_keyno;

						/*  
						 * 如果这是第一个分区键的子句，  
						 * 则没有前面的表达式；生成  
						 * 一个没有前缀的剪枝步骤。  
						 *  
						 * 注意，我们为 step_nullkeys 传递 NULL，因为  
						 * 当某些键为 NULL 时，我们不搜索列表/范围分区边界。  
						 */  
						if (fc_pc->keyno == 0)
						{
							Assert(fc_pc->op_strategy == fc_strat);
							fc_pc_steps = fc_get_steps_using_prefix(fc_context, fc_strat,
															  fc_pc->op_is_ne,
															  fc_pc->expr,
															  fc_pc->cmpfn,
															  NULL,
															  NIL);
							fc_opsteps = list_concat(fc_opsteps, fc_pc_steps);
							continue;
						}

						fc_eq_start = list_head(fc_eq_clauses);
						fc_le_start = list_head(fc_le_clauses);
						fc_ge_start = list_head(fc_ge_clauses);

						/*  
						 * 我们将子句按其分区键编号的升序  
						 * 排列到前缀中。  
						 */  
						for (fc_keyno = 0; fc_keyno < fc_pc->keyno; fc_keyno++)
						{
							fc_pk_has_clauses = false;

							/*  
							 * 来自 = 子句的表达式可以始终出现在  
							 * 前缀中，前提是它们来自一个早期键。  
							 */  
							for_each_cell(fc_lc1, fc_eq_clauses, fc_eq_start)
							{
								PartClauseInfo *fc_eqpc = lfirst(fc_lc1);

								if (fc_eqpc->keyno == fc_keyno)
								{
									fc_prefix = lappend(fc_prefix, fc_eqpc);
									fc_pk_has_clauses = true;
								}
								else
								{
									Assert(fc_eqpc->keyno > fc_keyno);
									break;
								}
							}
							fc_eq_start = fc_lc1;

							/*  
							 * 如果我们正在为 </<= 策略生成步骤，  
							 * 可以将其他 <= 子句添加到前缀中，  
							 * 前提是它们来自一个早期键。  
							 */  
							if (fc_strat == BTLessStrategyNumber ||
								fc_strat == BTLessEqualStrategyNumber)
							{
								for_each_cell(fc_lc1, fc_le_clauses, fc_le_start)
								{
									PartClauseInfo *fc_lepc = lfirst(fc_lc1);

									if (fc_lepc->keyno == fc_keyno)
									{
										fc_prefix = lappend(fc_prefix, fc_lepc);
										fc_pk_has_clauses = true;
									}
									else
									{
										Assert(fc_lepc->keyno > fc_keyno);
										break;
									}
								}
								fc_le_start = fc_lc1;
							}

							/*  
							 * 如果我们正在为 >/>= 策略生成步骤，  
							 * 可以将其他 >= 子句添加到前缀中，  
							 * 前提是它们来自一个早期键。  
							 */  
							if (fc_strat == BTGreaterStrategyNumber ||
								fc_strat == BTGreaterEqualStrategyNumber)
							{
								for_each_cell(fc_lc1, fc_ge_clauses, fc_ge_start)
								{
									PartClauseInfo *fc_gepc = lfirst(fc_lc1);

									if (fc_gepc->keyno == fc_keyno)
									{
										fc_prefix = lappend(fc_prefix, fc_gepc);
										fc_pk_has_clauses = true;
									}
									else
									{
										Assert(fc_gepc->keyno > fc_keyno);
										break;
									}
								}
								fc_ge_start = fc_lc1;
							}

							/*  
							 * 如果该键没有子句，前缀将不再有效。  
							 */  
							if (!fc_pk_has_clauses)
							{
								fc_prefix_valid = false;
								break;
							}
						}

						/*
						 * 如果prefix_valid，生成PartitionPruneStepOps。
						 * 否则，我们将不再找到有效子集的子分区键的子句；
						 * 放弃进一步为该策略生成分区修剪步骤。
						 *
						 * 如上所述，如果'prefix'包含多个
						 * 对于同一键的表达式，以下将
						 * 生成多个步骤，每个步骤对应不同键的表达式组合。
						 *
						 * 请注意，我们将NULL传递给step_nullkeys，因为
						 * 我们不搜索某些键为NULL的列表/范围分区边界。
						 */
						if (fc_prefix_valid)
						{
							Assert(fc_pc->op_strategy == fc_strat);
							fc_pc_steps = fc_get_steps_using_prefix(fc_context, fc_strat,
															  fc_pc->op_is_ne,
															  fc_pc->expr,
															  fc_pc->cmpfn,
															  NULL,
															  fc_prefix);
							fc_opsteps = list_concat(fc_opsteps, fc_pc_steps);
						}
						else
							break;
					}
				}
				break;
			}

		case PARTITION_STRATEGY_HASH:
			{
				List	   *fc_eq_clauses = fc_hash_clauses[HTEqualStrategyNumber];

				/* 对于哈希分区，我们只有=策略。 */
				if (fc_eq_clauses != NIL)
				{
					PartClauseInfo *fc_pc;
					List	   *fc_pc_steps;
					List	   *fc_prefix = NIL;
					int			fc_last_keyno;
					ListCell   *fc_lc1;

					/*
					 * 定位最大列的子句。这可能不属于
					 * 最后一个分区键，但它是属于最后一个分区键的
					 * 子句，在上面我们找到了一条子句。
					 */
					fc_pc = llast(fc_eq_clauses);

					/*
					 * 可能有多个子句与该分区键匹配；找到第一个这样的子句。
					 * 同时，将所有在该子句之前的子句添加到'prefix'。
					 */
					fc_last_keyno = fc_pc->keyno;
					foreach(fc_lc, fc_eq_clauses)
					{
						fc_pc = lfirst(fc_lc);
						if (fc_pc->keyno == fc_last_keyno)
							break;
						fc_prefix = lappend(fc_prefix, fc_pc);
					}

					/*
					 * 对于“最后”列的每个子句，在将
					 * 子句的自身表达式附加到'prefix'后，我们将
					 * 使用所生成的向量生成一个步骤，并将=作为其策略。
					 * 实际上，'prefix'可能包含同一键的多个子句，
					 * 在这种情况下，我们必须为不同键的表达式的
					 * 各种组合生成步骤，get_steps_using_prefix将为我们处理。
					 */
					for_each_cell(fc_lc1, fc_eq_clauses, fc_lc)
					{
						fc_pc = lfirst(fc_lc1);

						/*
						 * 请注意，我们将nullkeys传递给step_nullkeys，
						 * 因为我们需要告诉哈希分区边界搜索
						 * 函数我们找到的哪些键是NULL子句。
						 */
						Assert(fc_pc->op_strategy == HTEqualStrategyNumber);
						fc_pc_steps =
							fc_get_steps_using_prefix(fc_context,
												   HTEqualStrategyNumber,
												   false,
												   fc_pc->expr,
												   fc_pc->cmpfn,
												   fc_nullkeys,
												   fc_prefix);
						fc_opsteps = list_concat(fc_opsteps, fc_pc_steps);
					}
				}
				break;
			}

		default:
			elog(ERROR, "invalid partition strategy: %c",
				 fc_part_scheme->strategy);
			break;
	}

	return fc_opsteps;
}

/*
 * 如果分区键具有排序规则，则子句必须具有相同的
 * 输入排序规则。如果分区键不可排序，我们假设
 * 排序规则无关紧要，因为在执行分区时并未考虑排序规则，
 * 但子句仍可能由于其他输入为可排序类型而被分配了排序规则。
 *
 * 另请参见IndexCollMatchesExprColl。
 */
#define PartCollMatchesExprColl(partcoll, exprcoll) \
	((partcoll) == InvalidOid || (partcoll) == (exprcoll))

/*
 * match_clause_to_partition_key
 *		尝试将给定的“子句”与指定的分区键匹配。
 *
 * 返回值为：
 * * PARTCLAUSE_NOMATCH 如果子句与该分区键不匹配（但
 *   调用者应该继续尝试，因为它可能与后续的键匹配）。
 *   输出参数：未设置。
 *
 * * PARTCLAUSE_MATCH_CLAUSE 如果存在匹配。
 *   输出参数：*pc 被设置为为匹配的子句构造的 PartClauseInfo。
 *
 * * PARTCLAUSE_MATCH_NULLNESS 如果存在匹配，并且匹配的子句是
 *   “a IS NULL”或“a IS NOT NULL”子句。
 *   输出参数：在前一种情况下，*clause_is_not_null 被设置为 false，
 *   否则为 true。
 *
 * * PARTCLAUSE_MATCH_STEPS 如果存在匹配。
 *   输出参数：*clause_steps 被设置为为该子句递归生成的步骤列表。
 *
 * * PARTCLAUSE_MATCH_CONTRADICT 如果子句是自我矛盾的，即
 *   它可以证明返回 FALSE 或 NULL。
 *   输出参数：未设置。
 *
 * * PARTCLAUSE_UNSUPPORTED 如果子句与该分区键不匹配
 *   并且由于其形式或属性（例如包含易变函数），也不可能匹配任何其他键。
 *   输出参数：未设置。
 */
static PartClauseMatchStatus
fc_match_clause_to_partition_key(GeneratePruningStepsContext *fc_context,
							  Expr *fc_clause, Expr *fc_partkey, int fc_partkeyidx,
							  bool *fc_clause_is_not_null, PartClauseInfo **fc_pc,
							  List **fc_clause_steps)
{
	PartClauseMatchStatus fc_boolmatchstatus;
	PartitionScheme fc_part_scheme = fc_context->rel->part_scheme;
	Oid			fc_partopfamily = fc_part_scheme->partopfamily[fc_partkeyidx],
				fc_partcoll = fc_part_scheme->partcollation[fc_partkeyidx];
	Expr	   *fc_expr;
	bool		fc_noteq;

	/*
	 * 识别与布尔分区键匹配的特殊形状子句。
	 */
	fc_boolmatchstatus = fc_match_boolean_partition_clause(fc_partopfamily, fc_clause,
													 fc_partkey, &fc_expr, &fc_noteq);

	if (fc_boolmatchstatus == PARTCLAUSE_MATCH_CLAUSE)
	{
		PartClauseInfo *fc_partclause;

		/*
		 * 对于形式为 partkey IS NOT true 和 IS NOT false 的布尔测试，
		 * 我们反转这些子句。实际上，“partkey IS NOT true”
		 * 变为“partkey IS false OR partkey IS NULL”。我们通过
		 * 构建一个 OR BoolExpr 来做到这一点，并形成一个这样的子句，
		 * 并将其传递给 gen_partprune_steps_internal() 生成修剪步骤。
		 */
		if (fc_noteq)
		{
			List	   *fc_new_clauses;
			List	   *fc_or_clause;
			BooleanTest *fc_new_booltest = (BooleanTest *) copyObject(fc_clause);
			NullTest   *fc_nulltest;

			/* 我们期望 'noteq' 仅在 BooleanTests 中设为 true */
			Assert(IsA(fc_clause, BooleanTest));

			/* 反转布尔测试 */
			if (fc_new_booltest->booltesttype == IS_NOT_TRUE)
				fc_new_booltest->booltesttype = IS_FALSE;
			else if (fc_new_booltest->booltesttype == IS_NOT_FALSE)
				fc_new_booltest->booltesttype = IS_TRUE;
			else
			{
				/*
				 * 我们仅期望 match_boolean_partition_clause 针对
				 * IS_NOT_TRUE 和 IS_NOT_FALSE 匹配。IS_NOT_UNKNOWN
				 * 不受支持。
				 */
				Assert(false);
			}

			fc_nulltest = makeNode(NullTest);
			fc_nulltest->arg = copyObject(fc_partkey);
			fc_nulltest->nulltesttype = IS_NULL;
			fc_nulltest->argisrow = false;
			fc_nulltest->location = -1;

			fc_new_clauses = list_make2(fc_new_booltest, fc_nulltest);
			fc_or_clause = list_make1(makeBoolExpr(OR_EXPR, fc_new_clauses, -1));

			/* 最后，生成步骤 */
			*fc_clause_steps = fc_gen_partprune_steps_internal(fc_context, fc_or_clause);

			if (fc_context->contradictory)
				return PARTCLAUSE_MATCH_CONTRADICT; /* 不应该发生 */
			else if (*fc_clause_steps == NIL)
				return PARTCLAUSE_UNSUPPORTED;	/* 步骤生成失败 */
			return PARTCLAUSE_MATCH_STEPS;
		}

		fc_partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
		fc_partclause->keyno = fc_partkeyidx;
		/* 使用布尔相等运算符进行修剪。 */
		fc_partclause->opno = BooleanEqualOperator;
		fc_partclause->op_is_ne = false;
		fc_partclause->expr = fc_expr;
		/* 我们知道 expr 是布尔类型。 */
		fc_partclause->cmpfn = fc_part_scheme->partsupfunc[fc_partkeyidx].fn_oid;
		fc_partclause->op_strategy = InvalidStrategy;

		*fc_pc = fc_partclause;

		return PARTCLAUSE_MATCH_CLAUSE;
	}
	else if (IsA(fc_clause, OpExpr) &&
			 list_length(((OpExpr *) fc_clause)->args) == 2)
	{
		OpExpr	   *fc_opclause = (OpExpr *) fc_clause;
		Expr	   *fc_leftop,
				   *fc_rightop;
		Oid			fc_opno,
					fc_op_lefttype,
					fc_op_righttype,
					fc_negator = InvalidOid;
		Oid			fc_cmpfn;
		int			fc_op_strategy;
		bool		fc_is_opne_listp = false;
		PartClauseInfo *fc_partclause;

		fc_leftop = (Expr *) get_leftop(fc_clause);
		if (IsA(fc_leftop, RelabelType))
			fc_leftop = ((RelabelType *) fc_leftop)->arg;
		fc_rightop = (Expr *) get_rightop(fc_clause);
		if (IsA(fc_rightop, RelabelType))
			fc_rightop = ((RelabelType *) fc_rightop)->arg;
		fc_opno = fc_opclause->opno;

		/* 检查子句是否匹配该分区键 */
		if (equal(fc_leftop, fc_partkey))
			fc_expr = fc_rightop;
		else if (equal(fc_rightop, fc_partkey))
		{
			/*
			 * 仅在我们可以交换运算符以将
			 * partkey 放在左侧时才有用。如果我们不能这样做，则子句可以被视为
			 * UNSUPPORTED。即使其 leftop 匹配某个后续的 partkey，我们
			 * 现在知道它在右侧有 Vars，因此没有用。
			 */
			fc_opno = get_commutator(fc_opno);
			if (!OidIsValid(fc_opno))
				return PARTCLAUSE_UNSUPPORTED;
			fc_expr = fc_leftop;
		}
		else
			/* 子句与该分区键不匹配，但可能与下一个匹配。 */
			return PARTCLAUSE_NOMATCH;

		/*
		 * 分区键匹配也需要排序匹配。可能存在
		 * 多个具有相同表达式但不同排序的 partkeys，因此失败返回 NOMATCH。
		 */
		if (!PartCollMatchesExprColl(fc_partcoll, fc_opclause->inputcollid))
			return PARTCLAUSE_NOMATCH;

		/*
		 * 查看操作符是否与分区操作族相关。
		 *
		 * 通常我们只关心列为分区操作符族一部分的操作符。但是有一个例外：
		 * 不等操作符在任何操作符族中都没有列出，但它们的否定操作符（等于）是有列出的。如果我们找到它们，我们可以使用其中一个，但仅限于列表分区。
		 *
		 * 注意：在失败时我们报告NOMATCH，以防后面的partkey具有相同的表达式但不同的操作符族。这不太可能，但与具有不同排序规则的重复表达式相比，也没有更大的可能性。
		 */
		if (op_in_opfamily(fc_opno, fc_partopfamily))
		{
			get_op_opfamily_properties(fc_opno, fc_partopfamily, false,
									   &fc_op_strategy, &fc_op_lefttype,
									   &fc_op_righttype);
		}
		else
		{
			if (fc_part_scheme->strategy != PARTITION_STRATEGY_LIST)
				return PARTCLAUSE_NOMATCH;

			/* 查看否定操作符是否为等于 */
			fc_negator = get_negator(fc_opno);
			if (OidIsValid(fc_negator) && op_in_opfamily(fc_negator, fc_partopfamily))
			{
				get_op_opfamily_properties(fc_negator, fc_partopfamily, false,
										   &fc_op_strategy, &fc_op_lefttype,
										   &fc_op_righttype);
				if (fc_op_strategy == BTEqualStrategyNumber)
					fc_is_opne_listp = true;	/* 成功 */
			}

			/* 不，这是不等于也不是。 */
			if (!fc_is_opne_listp)
				return PARTCLAUSE_NOMATCH;
		}

		/*
		 * 仅允许严格操作符。这将保证过滤掉null值。（这个测试可能没有用，因为btree和hash比较操作符通常是严格的。）
		 */
		if (!op_strict(fc_opno))
			return PARTCLAUSE_UNSUPPORTED;

		/*
		 * 好的，我们与分区键匹配并且找到了合适的操作符。
		 * 检查另一个参数以查看它是否可以用于修剪。
		 *
		 * 在大多数情况下，我们可以返回UNSUPPORTED，因为不管匹配到哪个partkey，都会出现相同的失败。（特别是，现在我们已经成功匹配了操作条件的一侧与partkey，没有机会将另一侧匹配到另一个partkey，从而产生可用的结果，因为这将意味着两侧都有Vars。）
		 *
		 * 此外，如果我们因目标依赖原因拒绝一个参数，请设置*context的适当字段以报告这一点。我们将这些测试推迟到匹配partkey和操作符之后，以减少为那些最终不对修剪步骤做出贡献的子句设置上下文字段的概率。
		 *
		 * 首先，检查非Const参数。（我们假设任何不可变的子表达式已经被折叠为Const。）
		 */
		if (!IsA(fc_expr, Const))
		{
			Bitmapset  *fc_paramids;

			/*
			 * 在规划器中进行修剪时，我们只支持使用与常量的比较进行修剪。
			 * 我们不能基于任何不可变以外的内容进行修剪。（请注意，has_mutable_arg和has_exec_param不会为此目标值设置。）
			 */
			if (fc_context->target == PARTTARGET_PLANNER)
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 我们绝不能使用包含Vars的表达式进行修剪。
			 */
			if (contain_var_clause((Node *) fc_expr))
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 而且我们必须拒绝包含易变函数的任何内容。然而，稳定函数是可以的。
			 */
			if (contain_volatile_functions((Node *) fc_expr))
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 查看是否有任何执行参数。如果有，我们只能在每次扫描修剪期间使用此表达式。
			 */
			fc_paramids = fc_pull_exec_paramids(fc_expr);
			if (!bms_is_empty(fc_paramids))
			{
				fc_context->has_exec_param = true;
				if (fc_context->target != PARTTARGET_EXEC)
					return PARTCLAUSE_UNSUPPORTED;
			}
			else
			{
				/* 它是潜在可用的，但可变 */
				fc_context->has_mutable_arg = true;
			}
		}

		/*
		 * 检查比较操作符本身是否不可变。（我们假设在btree或hash操作类别中的任何内容至少是稳定的，但我们需要检查不可变性。）
		 */
		if (op_volatile(fc_opno) != PROVOLATILE_IMMUTABLE)
		{
			fc_context->has_mutable_op = true;

			/*
			 * 在规划器中进行修剪时，我们不能使用可变操作符进行修剪。
			 */
			if (fc_context->target == PARTTARGET_PLANNER)
				return PARTCLAUSE_UNSUPPORTED;
		}

		/*
		 * 现在根据类型查找要使用的过程。如果子句的
		 * 另一个参数与分区操作类声明的输入类型相同，我们可以使用
		 * 缓存的过程在PartitionKey中。如果不相同，则在同一
		 * 操作族中搜索跨类型的过程；如果不存在，则报告不匹配。
		 */
		if (fc_op_righttype == fc_part_scheme->partopcintype[fc_partkeyidx])
			fc_cmpfn = fc_part_scheme->partsupfunc[fc_partkeyidx].fn_oid;
		else
		{
			switch (fc_part_scheme->strategy)
			{
					/*
					 * 对于范围和列表分区，我们需要顺序
					 * 过程，其中左类型是分区键的类型，
					 * 右类型是子句的运算符的右类型。
					 */
				case PARTITION_STRATEGY_LIST:
				case PARTITION_STRATEGY_RANGE:
					fc_cmpfn =
						get_opfamily_proc(fc_part_scheme->partopfamily[fc_partkeyidx],
										  fc_part_scheme->partopcintype[fc_partkeyidx],
										  fc_op_righttype, BTORDER_PROC);
					break;

					/*
					 * 对于哈希分区，我们需要子句类型的
					 * 哈希过程。
					 */
				case PARTITION_STRATEGY_HASH:
					fc_cmpfn =
						get_opfamily_proc(fc_part_scheme->partopfamily[fc_partkeyidx],
										  fc_op_righttype, fc_op_righttype,
										  HASHEXTENDED_PROC);
					break;

				default:
					elog(ERROR, "invalid partition strategy: %c",
						 fc_part_scheme->strategy);
					fc_cmpfn = InvalidOid; /* 保持编译器安静 */
					break;
			}

			if (!OidIsValid(fc_cmpfn))
				return PARTCLAUSE_NOMATCH;
		}

		/*
		 * 构建子句，必要时传递否定运算符。
		 */
		fc_partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
		fc_partclause->keyno = fc_partkeyidx;
		if (fc_is_opne_listp)
		{
			Assert(OidIsValid(fc_negator));
			fc_partclause->opno = fc_negator;
			fc_partclause->op_is_ne = true;
			fc_partclause->op_strategy = InvalidStrategy;
		}
		else
		{
			fc_partclause->opno = fc_opno;
			fc_partclause->op_is_ne = false;
			fc_partclause->op_strategy = fc_op_strategy;
		}
		fc_partclause->expr = fc_expr;
		fc_partclause->cmpfn = fc_cmpfn;

		*fc_pc = fc_partclause;

		return PARTCLAUSE_MATCH_CLAUSE;
	}
	else if (IsA(fc_clause, ScalarArrayOpExpr))
	{
		ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_clause;
		Oid			fc_saop_op = fc_saop->opno;
		Oid			fc_saop_coll = fc_saop->inputcollid;
		Expr	   *fc_leftop = (Expr *) linitial(fc_saop->args),
				   *fc_rightop = (Expr *) lsecond(fc_saop->args);
		List	   *fc_elem_exprs,
				   *fc_elem_clauses;
		ListCell   *fc_lc1;

		if (IsA(fc_leftop, RelabelType))
			fc_leftop = ((RelabelType *) fc_leftop)->arg;

		/* 检查左侧是否与此分区键匹配 */
		if (!equal(fc_leftop, fc_partkey) ||
			!PartCollMatchesExprColl(fc_partcoll, fc_saop->inputcollid))
			return PARTCLAUSE_NOMATCH;

		/*
		 * 查看运算符是否与分区操作族相关。
		 *
		 * 在NOT IN（..）的情况下，我们得到一个“<>”，如果使用
		 * 列表分区并且能够确认其否定运算符是属于分区操作
		 * 家族的btree相等运算符，则我们处理它。与上述相同，对于不匹配的运算符报告NOMATCH。
		 */
		if (!op_in_opfamily(fc_saop_op, fc_partopfamily))
		{
			Oid			fc_negator;

			if (fc_part_scheme->strategy != PARTITION_STRATEGY_LIST)
				return PARTCLAUSE_NOMATCH;

			fc_negator = get_negator(fc_saop_op);
			if (OidIsValid(fc_negator) && op_in_opfamily(fc_negator, fc_partopfamily))
			{
				int			fc_strategy;
				Oid			fc_lefttype,
							fc_righttype;

				get_op_opfamily_properties(fc_negator, fc_partopfamily,
										   false, &fc_strategy,
										   &fc_lefttype, &fc_righttype);
				if (fc_strategy != BTEqualStrategyNumber)
					return PARTCLAUSE_NOMATCH;
			}
			else
				return PARTCLAUSE_NOMATCH;	/* 没有有用的否定运算符 */
		}

		/*
		 * 仅允许严格操作符。这将保证过滤掉null值。（这个测试可能没有用，因为btree和hash比较操作符通常是严格的。）
		 */
		if (!op_strict(fc_saop_op))
			return PARTCLAUSE_UNSUPPORTED;

		/*
		 * 好的，我们找到了与分区键匹配且合适的运算符。
		 * 检查数组参数以查看它是否可用于修剪。这
		 * 与普通OpExpr的逻辑相同。
		 */
		if (!IsA(fc_rightop, Const))
		{
			Bitmapset  *fc_paramids;

			/*
			 * 在规划器中进行修剪时，我们只支持使用与常量的比较进行修剪。
			 * 我们不能基于任何不可变以外的内容进行修剪。（请注意，has_mutable_arg和has_exec_param不会为此目标值设置。）
			 */
			if (fc_context->target == PARTTARGET_PLANNER)
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 我们绝不能使用包含Vars的表达式进行修剪。
			 */
			if (contain_var_clause((Node *) fc_rightop))
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 而且我们必须拒绝包含易变函数的任何内容。然而，稳定函数是可以的。
			 */
			if (contain_volatile_functions((Node *) fc_rightop))
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 查看是否有任何执行参数。如果有，我们只能在每次扫描修剪期间使用此表达式。
			 */
			fc_paramids = fc_pull_exec_paramids(fc_rightop);
			if (!bms_is_empty(fc_paramids))
			{
				fc_context->has_exec_param = true;
				if (fc_context->target != PARTTARGET_EXEC)
					return PARTCLAUSE_UNSUPPORTED;
			}
			else
			{
				/* 它是潜在可用的，但可变 */
				fc_context->has_mutable_arg = true;
			}
		}

		/*
		 * 检查比较操作符本身是否不可变。（我们假设在btree或hash操作类别中的任何内容至少是稳定的，但我们需要检查不可变性。）
		 */
		if (op_volatile(fc_saop_op) != PROVOLATILE_IMMUTABLE)
		{
			fc_context->has_mutable_op = true;

			/*
			 * 在规划器中进行修剪时，我们不能使用可变操作符进行修剪。
			 */
			if (fc_context->target == PARTTARGET_PLANNER)
				return PARTCLAUSE_UNSUPPORTED;
		}

		/*
		 * 检查数组参数的内容。
		 */
		fc_elem_exprs = NIL;
		if (IsA(fc_rightop, Const))
		{
			/*
			 * 对于常量数组，将元素转换为Const
			 * 节点的列表，每个数组元素一个（不包括空值）。
			 */
			Const	   *fc_arr = (Const *) fc_rightop;
			ArrayType  *fc_arrval;
			int16		fc_elemlen;
			bool		fc_elembyval;
			char		fc_elemalign;
			Datum	   *fc_elem_values;
			bool	   *fc_elem_nulls;
			int			fc_num_elems,
						fc_i;

			/* 如果数组本身为null，saop返回null */
			if (fc_arr->constisnull)
				return PARTCLAUSE_MATCH_CONTRADICT;

			fc_arrval = DatumGetArrayTypeP(fc_arr->constvalue);
			get_typlenbyvalalign(ARR_ELEMTYPE(fc_arrval),
								 &fc_elemlen, &fc_elembyval, &fc_elemalign);
			deconstruct_array(fc_arrval,
							  ARR_ELEMTYPE(fc_arrval),
							  fc_elemlen, fc_elembyval, fc_elemalign,
							  &fc_elem_values, &fc_elem_nulls,
							  &fc_num_elems);
			for (fc_i = 0; fc_i < fc_num_elems; fc_i++)
			{
				Const	   *fc_elem_expr;

				/*
				 * 一个空数组元素必须导致一个空比较结果，
				 * 因为 saop_op 是已知的严格。我们可以在
				 * useOr 的情况下忽略它，但否则它意味着自相矛盾。
				 */
				if (fc_elem_nulls[fc_i])
				{
					if (fc_saop->useOr)
						continue;
					return PARTCLAUSE_MATCH_CONTRADICT;
				}

				fc_elem_expr = makeConst(ARR_ELEMTYPE(fc_arrval), -1,
									  fc_arr->constcollid, fc_elemlen,
									  fc_elem_values[fc_i], false, fc_elembyval);
				fc_elem_exprs = lappend(fc_elem_exprs, fc_elem_expr);
			}
		}
		else if (IsA(fc_rightop, ArrayExpr))
		{
			ArrayExpr  *fc_arrexpr = castNode(ArrayExpr, fc_rightop);

			/*
			 * 对于嵌套的 ArrayExpr，我们不知道如何将实际的
			 * 标量值提取到一个扁平列表中，因此我们放弃对
			 * 这个 ScalarArrayOpExpr 的任何操作。
			 */
			if (fc_arrexpr->multidims)
				return PARTCLAUSE_UNSUPPORTED;

			/*
			 * 否则，我们可以直接使用元素值的列表。
			 */
			fc_elem_exprs = fc_arrexpr->elements;
		}
		else
		{
			/* 放弃任何其他子句类型。 */
			return PARTCLAUSE_UNSUPPORTED;
		}

		/*
		 * 现在生成一个子句列表，为每个数组元素生成
		 * 形式为 leftop saop_op elem_expr 的子句。
		 */
		fc_elem_clauses = NIL;
		foreach(fc_lc1, fc_elem_exprs)
		{
			Expr	   *fc_rightop = (Expr *) lfirst(fc_lc1),
					   *fc_elem_clause;

			fc_elem_clause = make_opclause(fc_saop_op, BOOLOID, false,
										fc_leftop, fc_rightop,
										InvalidOid, fc_saop_coll);
			fc_elem_clauses = lappend(fc_elem_clauses, fc_elem_clause);
		}

		/*
		 * 如果我们有一个 ANY 子句且多个元素，现在将子句列表
		 * 转换为一个 OR 表达式。
		 */
		if (fc_saop->useOr && list_length(fc_elem_clauses) > 1)
			fc_elem_clauses = list_make1(makeBoolExpr(OR_EXPR, fc_elem_clauses, -1));

		/* 最后，生成步骤 */
		*fc_clause_steps = fc_gen_partprune_steps_internal(fc_context, fc_elem_clauses);
		if (fc_context->contradictory)
			return PARTCLAUSE_MATCH_CONTRADICT;
		else if (*fc_clause_steps == NIL)
			return PARTCLAUSE_UNSUPPORTED;	/* 步骤生成失败 */
		return PARTCLAUSE_MATCH_STEPS;
	}
	else if (IsA(fc_clause, NullTest))
	{
		NullTest   *fc_nulltest = (NullTest *) fc_clause;
		Expr	   *fc_arg = fc_nulltest->arg;

		if (IsA(fc_arg, RelabelType))
			fc_arg = ((RelabelType *) fc_arg)->arg;

		/* arg 是否与这个分区键列匹配？ */
		if (!equal(fc_arg, fc_partkey))
			return PARTCLAUSE_NOMATCH;

		*fc_clause_is_not_null = (fc_nulltest->nulltesttype == IS_NOT_NULL);

		return PARTCLAUSE_MATCH_NULLNESS;
	}

	/*
	 * 如果我们到这里，那么返回值取决于上述
	 * match_boolean_partition_clause 调用的结果。如果调用返回
	 * PARTCLAUSE_UNSUPPORTED，那么我们要么没有处理布尔条件
	 * 要么这个布尔条件不适合修剪。由于这个条件没有
	 * 与这里支持的其他条件类型匹配，因此尝试
	 * 与任何其他分区键匹配是浪费时间，因此只需
	 * 返回 PARTCLAUSE_UNSUPPORTED。如果这个条件只是无法匹配
	 * 到这个分区键，那么它可能匹配其他的，因此返回
	 * PARTCLAUSE_NOMATCH。match_boolean_partition_clause
	 * 可能返回的唯一其他值是 PARTCLAUSE_MATCH_CLAUSE，
	 * 由于这个值已经在上面处理过，因此我们只需
	 * 返回 boolmatchstatus。
	 */
	return fc_boolmatchstatus;
}

/*
 * get_steps_using_prefix
 *		根据给定输入生成一系列 PartitionPruneStepOps。
 *
 * 'step_lastexpr' 和 'step_lastcmpfn' 是与我们有子句的最终分区键相关的表达式和比较函数。'prefix'
 * 是与给定 'step_lastexpr' 和 'step_lastcmpfn' 之前的分区键编号相关的一系列 PartClauseInfos。
 * 'prefix' 可能包含多个属于单一分区键的 PartClauseInfos。我们将为给定的 PartClauseInfos
 * 每个组合生成一个 PartitionPruneStepOp，每个分区键最多使用一个 PartClauseInfo。
 *
 * 对于 LIST 和 RANGE 分区表，调用者必须确保 step_nullkeys 为 NULL，并且 prefix 至少包含一个
 * 子句，用于每个在 'step_lastexpr' 和 'step_lastcmpfn' 所属键之前的分区键。
 *
 * 对于 HASH 分区表，调用者必须确保 'prefix' 为每个分区键包含至少一个子句，排除最终键
 * （最终键的表达式和比较函数在 'step_lastexpr' 和 'step_lastcmpfn' 中）。在 step_nullkeys 中设置的位
 * 可以替代 'prefix' 列表中任何给定键的子句。如果在 'step_nullkeys' 中为某个键设置了位，则在
 * 'prefix' 列表中必须不存在该键的 PartClauseInfo。
 *
 * 对于上述每种情况，调用者必须确保 'prefix' 中的 PartClauseInfos 按 keyno 升序排列。
 */
static List * fc_get_steps_using_prefix(GeneratePruningStepsContext *fc_context,
					   StrategyNumber fc_step_opstrategy,
					   bool fc_step_op_is_ne,
					   Expr *fc_step_lastexpr,
					   Oid fc_step_lastcmpfn,
					   Bitmapset *fc_step_nullkeys,
					   List *fc_prefix)
{
	/* 对于 RANGE 和 LIST 分区表，step_nullkeys 必须为空 */
	Assert(fc_step_nullkeys == NULL ||
		   fc_context->rel->part_scheme->strategy == PARTITION_STRATEGY_HASH);

	/*
	 * 当 'prefix' 是一个空列表时，不需要递归处理。这
	 * 在只有一个分区键列时发生。
	 */
	if (list_length(fc_prefix) == 0)
	{
		PartitionPruneStep *fc_step;

		fc_step = fc_gen_prune_step_op(fc_context,
								 fc_step_opstrategy,
								 fc_step_op_is_ne,
								 list_make1(fc_step_lastexpr),
								 list_make1_oid(fc_step_lastcmpfn),
								 fc_step_nullkeys);
		return list_make1(fc_step);
	}

	/* 递归生成每个子句组合的步骤。 */
	return fc_get_steps_using_prefix_recurse(fc_context,
										  fc_step_opstrategy,
										  fc_step_op_is_ne,
										  fc_step_lastexpr,
										  fc_step_lastcmpfn,
										  fc_step_nullkeys,
										  fc_prefix,
										  list_head(fc_prefix),
										  NIL, NIL);
}

/*
 * get_steps_using_prefix_recurse
 *		使用 'prefix' 列表中的 PartClauseInfos 从 'start' 单元格开始生成并返回一个 PartitionPruneStepOps 列表。
 *
 * 当 'prefix' 包含单个分区键的多个 PartClauseInfos 时，我们为每个重复的
 * PartClauseInfos 组合创建一个 PartitionPruneStepOp。返回的列表将包含每个唯一组合
 * 的 PartitionPruneStepOp，输入 PartClauseInfos 每个分区键最多一个。
 *
 * 'prefix' 是按 keyno 排序的 PartClauseInfos 输入列表。
 * 'start' 标记搜索 'prefix' 列表应从哪个单元格开始。
 * 'step_exprs' 和 'step_cmpfns' 各自包含我们从上一部分键的子句生成的表达式和比较函数。
 */
static List * fc_get_steps_using_prefix_recurse(GeneratePruningStepsContext *fc_context,
							   StrategyNumber fc_step_opstrategy,
							   bool fc_step_op_is_ne,
							   Expr *fc_step_lastexpr,
							   Oid fc_step_lastcmpfn,
							   Bitmapset *fc_step_nullkeys,
							   List *fc_prefix,
							   ListCell *fc_start,
							   List *fc_step_exprs,
							   List *fc_step_cmpfns)
{
	List	   *fc_result = NIL;
	ListCell   *fc_lc;
	int			fc_cur_keyno;
	int			fc_final_keyno;

	/* 实际上，递归将受限于 PARTITION_MAX_KEYS。 */
	check_stack_depth();

	Assert(fc_start != NULL);
	fc_cur_keyno = ((PartClauseInfo *) lfirst(fc_start))->keyno;
	fc_final_keyno = ((PartClauseInfo *) llast(fc_prefix))->keyno;

	/* 检查是否需要递归。 */
	if (fc_cur_keyno < fc_final_keyno)
	{
		PartClauseInfo *fc_pc;
		ListCell   *fc_next_start;

		/*
		 * 找到属于下一个分区键的第一个 PartClauseInfo，下一次递归调用必须从该点开始
		 * 遍历 prefix 列表。
		 */
		for_each_cell(fc_lc, fc_prefix, fc_start)
		{
			fc_pc = lfirst(fc_lc);

			if (fc_pc->keyno > fc_cur_keyno)
				break;
		}

		/* 记录下一次递归调用中开始迭代的位置 */
		fc_next_start = fc_lc;

		/*
		 * 对于每个 keyno 设置为 cur_keyno 的 PartClauseInfo，将其 expr 和
		 * cmpfn 分别添加到 step_exprs 和 step_cmpfns 中，并使用
		 * 'next_start' 作为 'prefix' 列表中的起始点进行递归。
		 */
		for_each_cell(fc_lc, fc_prefix, fc_start)
		{
			List	   *fc_moresteps;
			List	   *fc_step_exprs1,
					   *fc_step_cmpfns1;

			fc_pc = lfirst(fc_lc);
			if (fc_pc->keyno == fc_cur_keyno)
			{
				/* 保留原始 step_exprs 不变。 */
				fc_step_exprs1 = list_copy(fc_step_exprs);
				fc_step_exprs1 = lappend(fc_step_exprs1, fc_pc->expr);

				/* 保留原始 step_cmpfns 不变。 */
				fc_step_cmpfns1 = list_copy(fc_step_cmpfns);
				fc_step_cmpfns1 = lappend_oid(fc_step_cmpfns1, fc_pc->cmpfn);
			}
			else
			{
				/* 检查 'prefix' 列表是否排序正确 */
				Assert(fc_pc->keyno > fc_cur_keyno);
				break;
			}

			fc_moresteps = fc_get_steps_using_prefix_recurse(fc_context,
													   fc_step_opstrategy,
													   fc_step_op_is_ne,
													   fc_step_lastexpr,
													   fc_step_lastcmpfn,
													   fc_step_nullkeys,
													   fc_prefix,
													   fc_next_start,
													   fc_step_exprs1,
													   fc_step_cmpfns1);
			fc_result = list_concat(fc_result, fc_moresteps);

			list_free(fc_step_exprs1);
			list_free(fc_step_cmpfns1);
		}
	}
	else
	{
		/*
		 * 结束当前递归周期，开始生成步骤，为每个具有 cur_keyno
		 * 的子句生成一步，这些子句是从这里开始到列表末尾的所有子句。
		 * 请注意，对于哈希分区，step_nullkeys 允许非空，在这种情况下
		 * step_exprs 只会包含在 step_nullkeys 中未指定的分区键的表达式。
		 */
		Assert(list_length(fc_step_exprs) == fc_cur_keyno ||
			   !bms_is_empty(fc_step_nullkeys));

		/*
		 * 还要注意，对于哈希分区，每个分区键应该有等式子句或 IS NULL 子句，
		 * 所以如果某个分区键没有表达式，它将被指定在 step_nullkeys 中。
		 */
		Assert(fc_context->rel->part_scheme->strategy
			   != PARTITION_STRATEGY_HASH ||
			   list_length(fc_step_exprs) + 2 + bms_num_members(fc_step_nullkeys) ==
			   fc_context->rel->part_scheme->partnatts);
		for_each_cell(fc_lc, fc_prefix, fc_start)
		{
			PartClauseInfo *fc_pc = lfirst(fc_lc);
			PartitionPruneStep *fc_step;
			List	   *fc_step_exprs1,
					   *fc_step_cmpfns1;

			Assert(fc_pc->keyno == fc_cur_keyno);

			/* 保留原始 step_exprs 不变。 */
			fc_step_exprs1 = list_copy(fc_step_exprs);
			fc_step_exprs1 = lappend(fc_step_exprs1, fc_pc->expr);
			fc_step_exprs1 = lappend(fc_step_exprs1, fc_step_lastexpr);

			/* 保留原始 step_cmpfns 不变。 */
			fc_step_cmpfns1 = list_copy(fc_step_cmpfns);
			fc_step_cmpfns1 = lappend_oid(fc_step_cmpfns1, fc_pc->cmpfn);
			fc_step_cmpfns1 = lappend_oid(fc_step_cmpfns1, fc_step_lastcmpfn);

			fc_step = fc_gen_prune_step_op(fc_context,
									 fc_step_opstrategy, fc_step_op_is_ne,
									 fc_step_exprs1, fc_step_cmpfns1,
									 fc_step_nullkeys);
			fc_result = lappend(fc_result, fc_step);
		}
	}

	return fc_result;
}

/*
 * get_matching_hash_bounds
 *		确定与指定值匹配的哈希边界的偏移量，
 *		考虑到所有非空值来自包含兼容哈希等式操作符的子句，
 *		而任何为空的键来自 IS NULL 子句。
 *
 * 通常，该函数将返回一个匹配的边界偏移量，
 * 尽管如果给定模数的分区未设置，则可能返回没有匹配项。
 * 如果找到的子句数量未覆盖整个分区键，则需要返回所有偏移量。
 *
 * 'opstrategy' 如果非零，必须是 HTEqualStrategyNumber。
 *
 * 'values' 包含按分区键索引的 Datums，用于修剪。
 *
 * 'nvalues' 是 'values' 数组中的 Datum 数量。
 *
 * 'partsupfunc' 包含可以为 'values' 中所包含的类型生成正确哈希的分区哈希函数。
 *
 * 'nullkeys' 是为空的分区键的集合。
 */
static PruneStepResult * fc_get_matching_hash_bounds(PartitionPruneContext *fc_context,
						 StrategyNumber fc_opstrategy, Datum *fc_values, int fc_nvalues,
						 FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys)
{
	PruneStepResult *fc_result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
	PartitionBoundInfo fc_boundinfo = fc_context->boundinfo;
	int		   *fc_partindices = fc_boundinfo->indexes;
	int			fc_partnatts = fc_context->partnatts;
	bool		fc_isnull[PARTITION_MAX_KEYS];
	int			fc_i;
	uint64		fc_rowHash;
	int			fc_greatest_modulus;
	Oid		   *fc_partcollation = fc_context->partcollation;

	Assert(fc_context->strategy == PARTITION_STRATEGY_HASH);

	/*
	 * 对于哈希分区，我们只能根据与分区键的等式子句或 IS NULL 子句进行修剪。
	 * 只有在我们获得所有键的值时，才能进行修剪。
	 */
	if (fc_nvalues + bms_num_members(fc_nullkeys) == fc_partnatts)
	{
		/*
		 * 如果有任何值，它们必须来自包含与哈希分区兼容的等式操作符的子句。
		 */
		Assert(fc_opstrategy == HTEqualStrategyNumber || fc_nvalues == 0);

		for (fc_i = 0; fc_i < fc_partnatts; fc_i++)
			fc_isnull[fc_i] = bms_is_member(fc_i, fc_nullkeys);

		fc_rowHash = compute_partition_hash_value(fc_partnatts, fc_partsupfunc, fc_partcollation,
											   fc_values, fc_isnull);

		fc_greatest_modulus = fc_boundinfo->nindexes;
		if (fc_partindices[fc_rowHash % fc_greatest_modulus] >= 0)
			fc_result->bound_offsets =
				bms_make_singleton(fc_rowHash % fc_greatest_modulus);
	}
	else
	{
		/* 报告所有有效的偏移到 boundinfo->indexes 数组。 */
		fc_result->bound_offsets = bms_add_range(NULL, 0,
											  fc_boundinfo->nindexes - 1);
	}

	/*
	 * 既没有特殊的哈希空分区，也没有默认哈希分区。
	 */
	fc_result->scan_null = fc_result->scan_default = false;

	return fc_result;
}

/*
 * get_matching_list_bounds
 *		确定与指定值匹配的列表边界的偏移量，
 *		根据给定操作符策略的语义
 *
 * 如果存在默认分区且需要扫描，则在返回的结构中将设置scan_default。
 * 如果需要扫描特殊的接受null的分区，则将设置scan_null。
 *
 * 'opstrategy'如果非零，必须是btree策略编号。
 *
 * 'value'包含用于修剪的值。
 *
 * 'nvalues'如果非零，应该恰好为1，因为存在列表分区。
 *
 * 'partsupfunc'包含用于进行partition_list_bsearch的列表分区比较函数。
 *
 * 'nullkeys'是包含null的分区键的集合。
 */
static PruneStepResult * fc_get_matching_list_bounds(PartitionPruneContext *fc_context,
						 StrategyNumber fc_opstrategy, Datum fc_value, int fc_nvalues,
						 FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys)
{
	PruneStepResult *fc_result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
	PartitionBoundInfo fc_boundinfo = fc_context->boundinfo;
	int			fc_off,
				fc_minoff,
				fc_maxoff;
	bool		fc_is_equal;
	bool		fc_inclusive = false;
	Oid		   *fc_partcollation = fc_context->partcollation;

	Assert(fc_context->strategy == PARTITION_STRATEGY_LIST);
	Assert(fc_context->partnatts == 1);

	fc_result->scan_null = fc_result->scan_default = false;

	if (!bms_is_empty(fc_nullkeys))
	{
		/*
		 * null仅可能存在于一个分区中 - 该分区接受的值集合包括null 
		 * 或默认分区（如果前者不存在）。
		 */
		if (partition_bound_accepts_nulls(fc_boundinfo))
			fc_result->scan_null = true;
		else
			fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		return fc_result;
	}

	/*
	 * 如果没有数据项与键进行比较，但存在分区， 
	 * 则返回默认分区（如果存在）。
	 */
	if (fc_boundinfo->ndatums == 0)
	{
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		return fc_result;
	}

	fc_minoff = 0;
	fc_maxoff = fc_boundinfo->ndatums - 1;

	/*
	 * 如果没有值与boundinfo中的数据项进行比较， 
	 * 这意味着调用者请求了所有非null数据项的分区。 
	 * 添加*所有*分区的索引，包括默认分区（如果有）。
	 */
	if (fc_nvalues == 0)
	{
		Assert(fc_boundinfo->ndatums > 0);
		fc_result->bound_offsets = bms_add_range(NULL, 0,
											  fc_boundinfo->ndatums - 1);
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		return fc_result;
	}

	/* 来自<>操作符子句的值的特殊情况处理。 */
	if (fc_opstrategy == InvalidStrategy)
	{
		/*
		 * 首先匹配所有边界。我们将删除以下任何匹配的数据项。
		 */
		Assert(fc_boundinfo->ndatums > 0);
		fc_result->bound_offsets = bms_add_range(NULL, 0,
											  fc_boundinfo->ndatums - 1);

		fc_off = partition_list_bsearch(fc_partsupfunc, fc_partcollation, fc_boundinfo,
									 fc_value, &fc_is_equal);
		if (fc_off >= 0 && fc_is_equal)
		{

			/* 我们有匹配项。从结果中移除。 */
			Assert(fc_boundinfo->indexes[fc_off] >= 0);
			fc_result->bound_offsets = bms_del_member(fc_result->bound_offsets,
												   fc_off);
		}

		/* 如果有，始终包括默认分区。 */
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);

		return fc_result;
	}

	/*
	 * 对于范围查询，始终包括默认列表分区，因为
	 * 列表分区以不连续的方式划分键空间，并不是所有
	 * 在给定范围内的值都会被分配到分区。这对某些数据类型（例如整数类型）可能
	 * 在技术上不成立，但我们目前缺乏任何基础设施来提供
	 * 证明，使我们能够在这里做任何更聪明的事情。
	 */
	if (fc_opstrategy != BTEqualStrategyNumber)
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);

	switch (fc_opstrategy)
	{
		case BTEqualStrategyNumber:
			fc_off = partition_list_bsearch(fc_partsupfunc,
										 fc_partcollation,
										 fc_boundinfo, fc_value,
										 &fc_is_equal);
			if (fc_off >= 0 && fc_is_equal)
			{
				Assert(fc_boundinfo->indexes[fc_off] >= 0);
				fc_result->bound_offsets = bms_make_singleton(fc_off);
			}
			else
				fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
			return fc_result;

		case BTGreaterEqualStrategyNumber:
			fc_inclusive = true;
			/* fall through */
		case BTGreaterStrategyNumber:
			fc_off = partition_list_bsearch(fc_partsupfunc,
										 fc_partcollation,
										 fc_boundinfo, fc_value,
										 &fc_is_equal);
			if (fc_off >= 0)
			{
				/* 我们不希望匹配的数据项出现在结果中。 */
				if (!fc_is_equal || !fc_inclusive)
					fc_off++;
			}
			else
			{
				/*
				 * 这种情况意味着所有分区边界都更大，这意味着
				 * 所有分区都满足这个键。
				 */
				fc_off = 0;
			}

			/*
			 * off大于我们有的分区的数据项数量。
			 * 唯一可能包含匹配项的分区是
			 * 默认分区，但如果存在，我们必须在上面设置context->scan_default。
			 */
			if (fc_off > fc_boundinfo->ndatums - 1)
				return fc_result;

			fc_minoff = fc_off;
			break;

		case BTLessEqualStrategyNumber:
			fc_inclusive = true;
			/* fall through */
		case BTLessStrategyNumber:
			fc_off = partition_list_bsearch(fc_partsupfunc,
										 fc_partcollation,
										 fc_boundinfo, fc_value,
										 &fc_is_equal);
			if (fc_off >= 0 && fc_is_equal && !fc_inclusive)
				fc_off--;

			/*
			 * off小于所有非默认分区的数据项。
			 * 唯一可能包含匹配项的分区是
			 * 默认分区，但如果存在，我们必须在上面设置context->scan_default。
			 */
			if (fc_off < 0)
				return fc_result;

			fc_maxoff = fc_off;
			break;

		default:
			elog(ERROR, "invalid strategy number %d", fc_opstrategy);
			break;
	}

	Assert(fc_minoff >= 0 && fc_maxoff >= 0);
	fc_result->bound_offsets = bms_add_range(NULL, fc_minoff, fc_maxoff);
	return fc_result;
}


/*
 * get_matching_range_bounds
 *		确定与指定值匹配的范围边界的偏移量，
 *		根据给定操作符策略的语义
 *
 * 每个偏移量在结果中的数据都被视为将包含所需值的
 * 分区的上限。
 *
 * 如果存在默认分区，并且我们非常确定它需要被扫描，
 * 则在返回的结构中设置scan_default。我们*不*仅仅因为
 * 值与默认之外的分区所覆盖的键空间的部分匹配而设置它
 * （我们通常假设属于默认分区的空间）：在结合多个修剪步骤
 * 后得到的最终边界集可能会排除它，因此我们在其他地方推断
 * 其包含。
 *
 * 'opstrategy' 如果非零，必须是一个btree策略编号。
 *
 * 'values' 包含按分区键索引的Datums，用于修剪。
 *
 * 'nvalues'，'values'数组中Datums的数量。必须 <= context->partnatts。
 *
 * 'partsupfunc' 包含用于执行partition_range_datum_bsearch或
 * partition_rbound_datum_cmp的范围分区比较函数。
 *
 * 'nullkeys' 是一组空的分区键。
 */
static PruneStepResult * fc_get_matching_range_bounds(PartitionPruneContext *fc_context,
						  StrategyNumber fc_opstrategy, Datum *fc_values, int fc_nvalues,
						  FmgrInfo *fc_partsupfunc, Bitmapset *fc_nullkeys)
{
	PruneStepResult *fc_result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
	PartitionBoundInfo fc_boundinfo = fc_context->boundinfo;
	Oid		   *fc_partcollation = fc_context->partcollation;
	int			fc_partnatts = fc_context->partnatts;
	int		   *fc_partindices = fc_boundinfo->indexes;
	int			fc_off,
				fc_minoff,
				fc_maxoff;
	bool		fc_is_equal;
	bool		fc_inclusive = false;

	Assert(fc_context->strategy == PARTITION_STRATEGY_RANGE);
	Assert(fc_nvalues <= fc_partnatts);

	fc_result->scan_null = fc_result->scan_default = false;

	/*
	 * 如果没有要与键进行比较的datums，或者如果我们得到了一个IS NULL
	 * 子句，则返回默认分区（如果存在）。
	 */
	if (fc_boundinfo->ndatums == 0 || !bms_is_empty(fc_nullkeys))
	{
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		return fc_result;
	}

	fc_minoff = 0;
	fc_maxoff = fc_boundinfo->ndatums;

	/*
	 * 如果没有值与boundinfo中的datums进行比较，
	 * 这意味着调用者要求所有非空datums的分区。添加
	 * *所有*分区的索引，包括默认分区（如果存在）。
	 */
	if (fc_nvalues == 0)
	{
		/* 忽略未被任何分区覆盖的键空间 */
		if (fc_partindices[fc_minoff] < 0)
			fc_minoff++;
		if (fc_partindices[fc_maxoff] < 0)
			fc_maxoff--;

		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		Assert(fc_partindices[fc_minoff] >= 0 &&
			   fc_partindices[fc_maxoff] >= 0);
		fc_result->bound_offsets = bms_add_range(NULL, fc_minoff, fc_maxoff);

		return fc_result;
	}

	/*
	 * 如果查询没有约束所有键列，我们需要扫描默认分区（如果有）。
	 */
	if (fc_nvalues < fc_partnatts)
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);

	switch (fc_opstrategy)
	{
		case BTEqualStrategyNumber:
			/* 寻找与查找值相等的最小边界。 */
			fc_off = partition_range_datum_bsearch(fc_partsupfunc,
												fc_partcollation,
												fc_boundinfo,
												fc_nvalues, fc_values,
												&fc_is_equal);

			if (fc_off >= 0 && fc_is_equal)
			{
				if (fc_nvalues == fc_partnatts)
				{
					/* 只能有零个或一个匹配的分区。 */
					fc_result->bound_offsets = bms_make_singleton(fc_off + 1);
					return fc_result;
				}
				else
				{
					int			fc_saved_off = fc_off;

					/*
					 * 由于查找值仅包含键的前缀，
					 * 我们必须找到其他可能匹配前缀的边界。
					 * partition_range_datum_bsearch()返回其中一个的
					 * 偏移量，通过检查相邻边界找到其他边界。
					 */

					/*
					 * 首先找到小于查找值的最大边界。
					 */
					while (fc_off >= 1)
					{
						int32		fc_cmpval;

						fc_cmpval =
							partition_rbound_datum_cmp(fc_partsupfunc,
													   fc_partcollation,
													   fc_boundinfo->datums[fc_off - 1],
													   fc_boundinfo->kind[fc_off - 1],
													   fc_values, fc_nvalues);
						if (fc_cmpval != 0)
							break;
						fc_off--;
					}

					Assert(0 ==
						   partition_rbound_datum_cmp(fc_partsupfunc,
													  fc_partcollation,
													  fc_boundinfo->datums[fc_off],
													  fc_boundinfo->kind[fc_off],
													  fc_values, fc_nvalues));

					/*
					 * 如果我们知道它是查找值可能存在的分区的
					 * 上限，我们可以将'off'视为要包含在结果中的
					 * 最小边界的偏移量。它可能无法存在的一种情况是
					 * 如果边界或其前缀的匹配部分不包含。
					 */
					if (fc_boundinfo->kind[fc_off][fc_nvalues] ==
						PARTITION_RANGE_DATUM_MINVALUE)
						fc_off++;

					fc_minoff = fc_off;

					/*
					 * 现在找到大于查找值的最小边界。
					 */
					fc_off = fc_saved_off;
					while (fc_off < fc_boundinfo->ndatums - 1)
					{
						int32		fc_cmpval;

						fc_cmpval = partition_rbound_datum_cmp(fc_partsupfunc,
															fc_partcollation,
															fc_boundinfo->datums[fc_off + 1],
															fc_boundinfo->kind[fc_off + 1],
															fc_values, fc_nvalues);
						if (fc_cmpval != 0)
							break;
						fc_off++;
					}

					Assert(0 ==
						   partition_rbound_datum_cmp(fc_partsupfunc,
													  fc_partcollation,
													  fc_boundinfo->datums[fc_off],
													  fc_boundinfo->kind[fc_off],
													  fc_values, fc_nvalues));

					/*
					 * off + 1，然后将是要包含在结果中的最大边界的偏移量。
					 */
					fc_maxoff = fc_off + 1;
				}

				Assert(fc_minoff >= 0 && fc_maxoff >= 0);
				fc_result->bound_offsets = bms_add_range(NULL, fc_minoff, fc_maxoff);
			}
			else
			{
				/*
				 * 查找值落在某些边界的范围内
				 * boundinfo。'off' 将是小于等于查找值的最大边界的偏移量，因此将 off + 1 添加到结果中
				 * 作为可能包含查找值的唯一分区的上边界的偏移量。如果 'off' 是
				 * -1，表示所有边界都大于，则我们只需
				 * 添加第一个边界的偏移量，即 0。
				 */
				fc_result->bound_offsets = bms_make_singleton(fc_off + 1);
			}

			return fc_result;

		case BTGreaterEqualStrategyNumber:
			fc_inclusive = true;
			/* fall through */
		case BTGreaterStrategyNumber:

			/*
			 * 查找大于或等于查找值的最小边界并
			 * 将 minoff 设置为其偏移量。
			 */
			fc_off = partition_range_datum_bsearch(fc_partsupfunc,
												fc_partcollation,
												fc_boundinfo,
												fc_nvalues, fc_values,
												&fc_is_equal);
			if (fc_off < 0)
			{
				/*
				 * 所有边界都大于查找值，因此将
				 * 所有边界包括在结果中。
				 */
				fc_minoff = 0;
			}
			else
			{
				if (fc_is_equal && fc_nvalues < fc_partnatts)
				{
					/*
					 * 由于查找值只包含键的前缀，
					 * 我们必须找到其他可能匹配的边界
					 * 前缀。partition_range_datum_bsearch() 返回
					 * 其中一个的偏移量，通过检查相邻的
					 * 边界找到其他边界。
					 *
					 * 基于查找值是否包含在内，
					 * 我们必须将所有这些边界的索引包括在結果中
					 * （即，将 minoff 设置为最小边界的索引）
					 * 或找到大于查找值的最小边界并将 minoff 设置为该边界。
					 */
					while (fc_off >= 1 && fc_off < fc_boundinfo->ndatums - 1)
					{
						int32		fc_cmpval;
						int			fc_nextoff;

						fc_nextoff = fc_inclusive ? fc_off - 1 : fc_off + 1;
						fc_cmpval =
							partition_rbound_datum_cmp(fc_partsupfunc,
													   fc_partcollation,
													   fc_boundinfo->datums[fc_nextoff],
													   fc_boundinfo->kind[fc_nextoff],
													   fc_values, fc_nvalues);
						if (fc_cmpval != 0)
							break;

						fc_off = fc_nextoff;
					}

					Assert(0 ==
						   partition_rbound_datum_cmp(fc_partsupfunc,
													  fc_partcollation,
													  fc_boundinfo->datums[fc_off],
													  fc_boundinfo->kind[fc_off],
													  fc_values, fc_nvalues));

					fc_minoff = fc_inclusive ? fc_off : fc_off + 1;
				}
				else
				{

					/*
					 * 查找值落在某些边界的范围内
                     * boundinfo。'off' 将是小于等于查找值的最大
                     * 边界的偏移量，因此将 off + 1 添加到
                     * 结果中，作为可能包含查找值的最小
                     * 分区的上边界的偏移量。
					 */
					fc_minoff = fc_off + 1;
				}
			}
			break;

		case BTLessEqualStrategyNumber:
			fc_inclusive = true;
			/* fall through */
		case BTLessStrategyNumber:

			/*
			 * 查找小于或等于查找值的最大边界并
			 * 将 maxoff 设置为其偏移量。
			 */
			fc_off = partition_range_datum_bsearch(fc_partsupfunc,
												fc_partcollation,
												fc_boundinfo,
												fc_nvalues, fc_values,
												&fc_is_equal);
			if (fc_off >= 0)
			{
				/*
				 * 请参见上面的注释。
				 */
				if (fc_is_equal && fc_nvalues < fc_partnatts)
				{
					while (fc_off >= 1 && fc_off < fc_boundinfo->ndatums - 1)
					{
						int32		fc_cmpval;
						int			fc_nextoff;

						fc_nextoff = fc_inclusive ? fc_off + 1 : fc_off - 1;
						fc_cmpval = partition_rbound_datum_cmp(fc_partsupfunc,
															fc_partcollation,
															fc_boundinfo->datums[fc_nextoff],
															fc_boundinfo->kind[fc_nextoff],
															fc_values, fc_nvalues);
						if (fc_cmpval != 0)
							break;

						fc_off = fc_nextoff;
					}

					Assert(0 ==
						   partition_rbound_datum_cmp(fc_partsupfunc,
													  fc_partcollation,
													  fc_boundinfo->datums[fc_off],
													  fc_boundinfo->kind[fc_off],
													  fc_values, fc_nvalues));

					fc_maxoff = fc_inclusive ? fc_off + 1 : fc_off;
				}

				/*
				 * 查找值落在某些边界的范围内
				 * boundinfo。'off' 将是小于等于查找值的最大边界的偏移量，因此将 off + 1 添加到结果中
				 * 作为可能包含查找值的最大分区的上边界的偏移量。如果查找
				 * 值正好匹配边界，但它不包含在内，则无需添加相邻的分区。
				 */
				else if (!fc_is_equal || fc_inclusive)
					fc_maxoff = fc_off + 1;
				else
					fc_maxoff = fc_off;
			}
			else
			{
				/*
				 * 'off' 是 -1，表示所有边界都大于，因此只需
				 * 将第一个边界的偏移量设置为 maxoff。
				 */
				fc_maxoff = fc_off + 1;
			}
			break;

		default:
			elog(ERROR, "invalid strategy number %d", fc_opstrategy);
			break;
	}

	Assert(fc_minoff >= 0 && fc_minoff <= fc_boundinfo->ndatums);
	Assert(fc_maxoff >= 0 && fc_maxoff <= fc_boundinfo->ndatums);

	/*
	 * 如果要返回的最小分区的下界为 MINVALUE（负无穷大），
	 * 则将其递增到指向下一个有限边界
	 * （假设为上界），以便我们不小心最终
	 * 扫描默认分区。
	 */
	if (fc_minoff < fc_boundinfo->ndatums && fc_partindices[fc_minoff] < 0)
	{
		int			fc_lastkey = fc_nvalues - 1;

		if (fc_boundinfo->kind[fc_minoff][fc_lastkey] ==
			PARTITION_RANGE_DATUM_MINVALUE)
		{
			fc_minoff++;
			Assert(fc_boundinfo->indexes[fc_minoff] >= 0);
		}
	}

	/*
	 * 如果之前最大的分区的上界为 MAXVALUE（正无穷大）
	 * （这仅在多列范围分区中可能），我们将其扫描切换为最大分区
	 * 返回。再次强调，以便我们不小心最终扫描默认分区。
	 */
	if (fc_maxoff >= 1 && fc_partindices[fc_maxoff] < 0)
	{
		int			fc_lastkey = fc_nvalues - 1;

		if (fc_boundinfo->kind[fc_maxoff - 1][fc_lastkey] ==
			PARTITION_RANGE_DATUM_MAXVALUE)
		{
			fc_maxoff--;
			Assert(fc_boundinfo->indexes[fc_maxoff] >= 0);
		}
	}

	Assert(fc_minoff >= 0 && fc_maxoff >= 0);
	if (fc_minoff <= fc_maxoff)
		fc_result->bound_offsets = bms_add_range(NULL, fc_minoff, fc_maxoff);

	return fc_result;
}

/*
 * pull_exec_paramids
 *		返回一个包含所有参数的 paramids 的 Bitmapset，参数的 paramkind = PARAM_EXEC 在 'expr' 中。
 */
static Bitmapset * fc_pull_exec_paramids(Expr *fc_expr)
{
	Bitmapset  *fc_result = NULL;

	(void) fc_pull_exec_paramids_walker((Node *) fc_expr, &fc_result);

	return fc_result;
}

static bool fc_pull_exec_paramids_walker(Node *fc_node, Bitmapset **fc_context)
{
	if (fc_node == NULL)
		return false;
	if (IsA(fc_node, Param))
	{
		Param	   *fc_param = (Param *) fc_node;

		if (fc_param->paramkind == PARAM_EXEC)
			*fc_context = bms_add_member(*fc_context, fc_param->paramid);
		return false;
	}
	return expression_tree_walker(fc_node, fc_pull_exec_paramids_walker,
								  (void *) fc_context);
}

/*
 * get_partkey_exec_paramids
 *		遍历给定的剪枝步骤，找出使用了哪些执行参数。
 *
 * 返回一个 Param ID 的 Bitmapset。
 */
static Bitmapset * fc_get_partkey_exec_paramids(List *fc_steps)
{
	Bitmapset  *fc_execparamids = NULL;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_steps)
	{
		PartitionPruneStepOp *fc_step = (PartitionPruneStepOp *) lfirst(fc_lc);
		ListCell   *fc_lc2;

		if (!IsA(fc_step, PartitionPruneStepOp))
			continue;

		foreach(fc_lc2, fc_step->exprs)
		{
			Expr	   *fc_expr = lfirst(fc_lc2);

			/* 对于普通的常量，我们可以快速处理 */
			if (!IsA(fc_expr, Const))
				fc_execparamids = bms_join(fc_execparamids,
										fc_pull_exec_paramids(fc_expr));
		}
	}

	return fc_execparamids;
}

/*
 * perform_pruning_base_step
 *		确定满足 'opstep' 中指定条件的数据项索引。
 *
 * 结果还包括是否需要扫描特殊的接受空值和/或默认分区。
 */
static PruneStepResult * fc_perform_pruning_base_step(PartitionPruneContext *fc_context,
						  PartitionPruneStepOp *fc_opstep)
{
	ListCell   *fc_lc1,
			   *fc_lc2;
	int			fc_keyno,
				fc_nvalues;
	Datum		fc_values[PARTITION_MAX_KEYS];
	FmgrInfo   *fc_partsupfunc;
	int			fc_stateidx;

	/*
	 * 表达式和比较函数的数量应该是相同的。
	 */
	Assert(list_length(fc_opstep->exprs) == list_length(fc_opstep->cmpfns));

	fc_nvalues = 0;
	fc_lc1 = list_head(fc_opstep->exprs);
	fc_lc2 = list_head(fc_opstep->cmpfns);

	/*
	 * 生成将在下面调用的 get_matching_*_bounds 函数中使用的分区查找键。
	 */
	for (fc_keyno = 0; fc_keyno < fc_context->partnatts; fc_keyno++)
	{
		/*
		 * 对于哈希分区，某些键的值可能在操作符子句中未提供，但规划器发现它们出现在 IS NULL 子句中。
		 */
		if (bms_is_member(fc_keyno, fc_opstep->nullkeys))
			continue;

		/*
		 * 对于范围分区，我们必须仅使用所有分区键或其前缀的值进行剪枝。
		 */
		if (fc_keyno > fc_nvalues && fc_context->strategy == PARTITION_STRATEGY_RANGE)
			break;

		if (fc_lc1 != NULL)
		{
			Expr	   *fc_expr;
			Datum		fc_datum;
			bool		fc_isnull;
			Oid			fc_cmpfn;

			fc_expr = lfirst(fc_lc1);
			fc_stateidx = PruneCxtStateIdx(fc_context->partnatts,
										fc_opstep->step.step_id, fc_keyno);
			fc_partkey_datum_from_expr(fc_context, fc_expr, fc_stateidx,
									&fc_datum, &fc_isnull);

			/*
			 * 由于我们仅允许在剪枝步骤中使用严格操作符，任何空值比较值必须导致比较失败，
			 * 以便没有分区能够匹配。
			 */
			if (fc_isnull)
			{
				PruneStepResult *fc_result;

				fc_result = (PruneStepResult *) palloc(sizeof(PruneStepResult));
				fc_result->bound_offsets = NULL;
				fc_result->scan_default = false;
				fc_result->scan_null = false;

				return fc_result;
			}

			/* 设置 stepcmpfuncs 条目，除非我们已经完成了这项工作 */
			fc_cmpfn = lfirst_oid(fc_lc2);
			Assert(OidIsValid(fc_cmpfn));
			if (fc_cmpfn != fc_context->stepcmpfuncs[fc_stateidx].fn_oid)
			{
				/*
				 * 如果所需的支持函数与关系的分区键中缓存的相同，则复制缓存的 FmgrInfo。
				 * 否则（即当我们进行跨类型比较时），需要进行实际查找。
				 */
				if (fc_cmpfn == fc_context->partsupfunc[fc_keyno].fn_oid)
					fmgr_info_copy(&fc_context->stepcmpfuncs[fc_stateidx],
								   &fc_context->partsupfunc[fc_keyno],
								   fc_context->ppccontext);
				else
					fmgr_info_cxt(fc_cmpfn, &fc_context->stepcmpfuncs[fc_stateidx],
								  fc_context->ppccontext);
			}

			fc_values[fc_keyno] = fc_datum;
			fc_nvalues++;

			fc_lc1 = lnext(fc_opstep->exprs, fc_lc1);
			fc_lc2 = lnext(fc_opstep->cmpfns, fc_lc2);
		}
	}

	/*
	 * 将 partsupfunc 指向该步骤的第 0 个键的条目；任何额外的支持函数（如果有）紧随其后。
	 */
	fc_stateidx = PruneCxtStateIdx(fc_context->partnatts, fc_opstep->step.step_id, 0);
	fc_partsupfunc = &fc_context->stepcmpfuncs[fc_stateidx];

	switch (fc_context->strategy)
	{
		case PARTITION_STRATEGY_HASH:
			return fc_get_matching_hash_bounds(fc_context,
											fc_opstep->opstrategy,
											fc_values, fc_nvalues,
											fc_partsupfunc,
											fc_opstep->nullkeys);

		case PARTITION_STRATEGY_LIST:
			return fc_get_matching_list_bounds(fc_context,
											fc_opstep->opstrategy,
											fc_values[0], fc_nvalues,
											&fc_partsupfunc[0],
											fc_opstep->nullkeys);

		case PARTITION_STRATEGY_RANGE:
			return fc_get_matching_range_bounds(fc_context,
											 fc_opstep->opstrategy,
											 fc_values, fc_nvalues,
											 fc_partsupfunc,
											 fc_opstep->nullkeys);

		default:
			elog(ERROR, "unexpected partition strategy: %d",
				 (int) fc_context->strategy);
			break;
	}

	return NULL;
}

/*
 * perform_pruning_combine_step
 *		通过使用指定的组合方法，将从由 cstep->source_stepids 确定的步骤组合得到的数据项索引
 *
 * 由于 cstep 可能引用早期步骤的结果，因此我们在这里也接收 step_results。
 */
static PruneStepResult * fc_perform_pruning_combine_step(PartitionPruneContext *fc_context,
							 PartitionPruneStepCombine *fc_cstep,
							 PruneStepResult **fc_step_results)
{
	PruneStepResult *fc_result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
	bool		fc_firststep;
	ListCell   *fc_lc1;

	/*
	 * 没有任何源步骤的组合步骤意味着不执行任何分区剪枝。在这种情况下返回所有数据项索引。
	 */
	if (fc_cstep->source_stepids == NIL)
	{
		PartitionBoundInfo fc_boundinfo = fc_context->boundinfo;

		fc_result->bound_offsets =
			bms_add_range(NULL, 0, fc_boundinfo->nindexes - 1);
		fc_result->scan_default = partition_bound_has_default(fc_boundinfo);
		fc_result->scan_null = partition_bound_accepts_nulls(fc_boundinfo);
		return fc_result;
	}

	switch (fc_cstep->combineOp)
	{
		case PARTPRUNE_COMBINE_UNION:
			foreach(fc_lc1, fc_cstep->source_stepids)
			{
				int			fc_step_id = lfirst_int(fc_lc1);
				PruneStepResult *fc_step_result;

				/*
				 * step_results[step_id] 必须包含有效的结果，这通过 cstep 的 step_id 大于
				 * step_id 以及各个步骤结果按其 step_ids 的顺序评估来确认。
				 */
				if (fc_step_id >= fc_cstep->step.step_id)
					elog(ERROR, "invalid pruning combine step argument");
				fc_step_result = fc_step_results[fc_step_id];
				Assert(fc_step_result != NULL);

				/* 记录此步骤的任何附加数据项索引 */
				fc_result->bound_offsets = bms_add_members(fc_result->bound_offsets,
														fc_step_result->bound_offsets);

				/* 更新是否扫描空值和默认分区。 */
				if (!fc_result->scan_null)
					fc_result->scan_null = fc_step_result->scan_null;
				if (!fc_result->scan_default)
					fc_result->scan_default = fc_step_result->scan_default;
			}
			break;

		case PARTPRUNE_COMBINE_INTERSECT:
			fc_firststep = true;
			foreach(fc_lc1, fc_cstep->source_stepids)
			{
				int			fc_step_id = lfirst_int(fc_lc1);
				PruneStepResult *fc_step_result;

				if (fc_step_id >= fc_cstep->step.step_id)
					elog(ERROR, "invalid pruning combine step argument");
				fc_step_result = fc_step_results[fc_step_id];
				Assert(fc_step_result != NULL);

				if (fc_firststep)
				{
					/* 第一次复制步骤的结果。 */
					fc_result->bound_offsets =
						bms_copy(fc_step_result->bound_offsets);
					fc_result->scan_null = fc_step_result->scan_null;
					fc_result->scan_default = fc_step_result->scan_default;
					fc_firststep = false;
				}
				else
				{
					/* 记录两个步骤共有的数据项索引 */
					fc_result->bound_offsets =
						bms_int_members(fc_result->bound_offsets,
										fc_step_result->bound_offsets);

					/* 更新是否扫描空值和默认分区。 */
					if (fc_result->scan_null)
						fc_result->scan_null = fc_step_result->scan_null;
					if (fc_result->scan_default)
						fc_result->scan_default = fc_step_result->scan_default;
				}
			}
			break;
	}

	return fc_result;
}

/*
 * match_boolean_partition_clause
 *
 * 如果我们能够将该子句与分区键匹配为特殊形状的布尔子句，则将*outconst设置为包含true或false值的Const，
 * 根据该子句是否为“not”形式，即“is not true”或“is not false”，设置*noteq，并返回PARTCLAUSE_MATCH_CLAUSE。
 * 如果该子句不是布尔子句或布尔子句不适合分区剪裁，则返回PARTCLAUSE_UNSUPPORTED。
 * 如果它是布尔资格但仅仅不匹配此分区键，则返回PARTCLAUSE_NOMATCH。
 * 在后两种情况下，*outconst被设置为NULL。
 */
static PartClauseMatchStatus
fc_match_boolean_partition_clause(Oid fc_partopfamily, Expr *fc_clause, Expr *fc_partkey,
							   Expr **fc_outconst, bool *fc_noteq)
{
	Expr	   *fc_leftop;

	*fc_outconst = NULL;
	*fc_noteq = false;

	if (!IsBooleanOpfamily(fc_partopfamily))
		return PARTCLAUSE_UNSUPPORTED;

	if (IsA(fc_clause, BooleanTest))
	{
		BooleanTest *fc_btest = (BooleanTest *) fc_clause;

		/* 仅IS [NOT] TRUE/FALSE对我们有用 */
		if (fc_btest->booltesttype == IS_UNKNOWN ||
			fc_btest->booltesttype == IS_NOT_UNKNOWN)
			return PARTCLAUSE_UNSUPPORTED;

		fc_leftop = fc_btest->arg;
		if (IsA(fc_leftop, RelabelType))
			fc_leftop = ((RelabelType *) fc_leftop)->arg;

		if (equal(fc_leftop, fc_partkey))
		{
			switch (fc_btest->booltesttype)
			{
				case IS_NOT_TRUE:
					*fc_noteq = true;
					/* fall through */
				case IS_TRUE:
					*fc_outconst = (Expr *) makeBoolConst(true, false);
					break;
				case IS_NOT_FALSE:
					*fc_noteq = true;
					/* fall through */
				case IS_FALSE:
					*fc_outconst = (Expr *) makeBoolConst(false, false);
					break;
				default:
					return PARTCLAUSE_UNSUPPORTED;
			}
		}
		if (*fc_outconst)
			return PARTCLAUSE_MATCH_CLAUSE;
	}
	else
	{
		bool		fc_is_not_clause = is_notclause(fc_clause);

		fc_leftop = fc_is_not_clause ? get_notclausearg(fc_clause) : fc_clause;

		if (IsA(fc_leftop, RelabelType))
			fc_leftop = ((RelabelType *) fc_leftop)->arg;

		/* 与分区键进行比较，并构造一个子句... */
		if (equal(fc_leftop, fc_partkey))
			*fc_outconst = (Expr *) makeBoolConst(!fc_is_not_clause, false);
		else if (equal(negate_clause((Node *) fc_leftop), fc_partkey))
			*fc_outconst = (Expr *) makeBoolConst(fc_is_not_clause, false);

		if (*fc_outconst)
			return PARTCLAUSE_MATCH_CLAUSE;
	}

	return PARTCLAUSE_NOMATCH;
}

/*
 * partkey_datum_from_expr
 *		评估表达式以进行潜在的分区剪裁
 *
 * 评估'expr'；将*value和*isnull设置为结果Datum和nullflag。
 *
 * 如果expr不是Const，则其ExprState位于上下文exprstate数组的stateidx中。
 *
 * 请注意，评估的结果可能在上下文->exprcontext的每个元组内存上下文中，
 * 而且我们可能在这里泄漏了其他内存。
 * 在完成剪裁操作后，必须通过重置该ExprContext来回收这段内存（参见execPartition.c）。
 */
static void fc_partkey_datum_from_expr(PartitionPruneContext *fc_context,
						Expr *fc_expr, int fc_stateidx,
						Datum *fc_value, bool *fc_isnull)
{
	if (IsA(fc_expr, Const))
	{
		/* 我们总是可以确定常量的值 */
		Const	   *fc_con = (Const *) fc_expr;

		*fc_value = fc_con->constvalue;
		*fc_isnull = fc_con->constisnull;
	}
	else
	{
		ExprState  *fc_exprstate;
		ExprContext *fc_ectx;

		/*
		 * 除非调用者传递了有效的ExprContext，
		 * 否则我们在步骤中应该永远不会看到非Const。
		 *
		 * 当context->planstate有效时，context->exprcontext与
		 * context->planstate->ps_ExprContext相同。
		 */
		Assert(fc_context->planstate != NULL || fc_context->exprcontext != NULL);
		Assert(fc_context->planstate == NULL ||
			   (fc_context->exprcontext == fc_context->planstate->ps_ExprContext));

		fc_exprstate = fc_context->exprstates[fc_stateidx];
		fc_ectx = fc_context->exprcontext;
		*fc_value = ExecEvalExprSwitchContext(fc_exprstate, fc_ectx, fc_isnull);
	}
}
